From 86fd3a3247ec964a2d32adb330d16b315c32c514 Mon Sep 17 00:00:00 2001 From: anuruddhal Date: Wed, 29 Jan 2025 10:38:02 +0530 Subject: [PATCH] Add support for IAM role --- s3/Dependencies.toml | 2 +- s3/Module.md | 10 ++- s3/client.bal | 171 +++++++++++++++++++++++-------------------- s3/constants.bal | 9 ++- s3/records.bal | 30 ++++++-- s3/utils.bal | 79 +++++++++++++++----- 6 files changed, 194 insertions(+), 107 deletions(-) diff --git a/s3/Dependencies.toml b/s3/Dependencies.toml index bfa83fd..dd9ba7c 100644 --- a/s3/Dependencies.toml +++ b/s3/Dependencies.toml @@ -319,7 +319,7 @@ modules = [ [[package]] org = "ballerinax" name = "aws.s3" -version = "3.3.0" +version = "3.4.0" dependencies = [ {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "http"}, diff --git a/s3/Module.md b/s3/Module.md index e084fa4..c2585b8 100644 --- a/s3/Module.md +++ b/s3/Module.md @@ -29,6 +29,14 @@ s3:ConnectionConfig amazonS3Config = { s3:Client amazonS3Client = check new(amazonS3Config); ``` +IAM role-based authentication can be used as below if the code is running within an EC2 instance. +```ballerina +s3:ConnectionConfig amazonS3Config = { + authType: s3:EC2_IAM_ROLE, + region: +} ; +``` + ### Step 3: Invoke connector operation 1. Now you can use the operations available within the connector. Note that they are in the form of remote operations. Following is an example on how to create a bucket using the connector. @@ -42,4 +50,4 @@ Following is an example on how to create a bucket using the connector. ``` 2. Use `bal run` command to compile and run the Ballerina program. -**[You can find a list of samples here](https://github.com/ballerina-platform/module-ballerinax-aws.s3/tree/master/samples)** +**[You can find a list of samples here](https://github.com/ballerina-platform/module-ballerinax-aws.s3/tree/master/examples)** diff --git a/s3/client.bal b/s3/client.bal index ba4242d..65d0acc 100644 --- a/s3/client.bal +++ b/s3/client.bal @@ -17,7 +17,6 @@ import ballerina/http; import ballerina/io; -import ballerina/regex; import ballerinax/'client.config; # Ballerina Amazon S3 connector provides the capability to access AWS S3 API. @@ -29,37 +28,49 @@ public isolated client class Client { private final string secretAccessKey; private final string region; private final string amazonHost; + private final AWS_STATIC_AUTH|EC2_IAM_ROLE authType; + private final string? sessionToken; private final http:Client amazonS3; # Initializes the connector. During initialization you have to pass access key id and secret access key # Create an AWS account and obtain tokens following # [this guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html). - # + # # + amazonS3Config - Configuration required to initialize the client # + httpConfig - HTTP configuration # + return - An error on failure of initialization or else `()` public isolated function init(ConnectionConfig config) returns error? { self.region = (config?.region is string) ? (config?.region) : DEFAULT_REGION; - self.amazonHost = self.region != DEFAULT_REGION ? regex:replaceFirst(AMAZON_AWS_HOST, SERVICE_NAME, - SERVICE_NAME + "." + self.region) : AMAZON_AWS_HOST; + self.amazonHost = self.region != DEFAULT_REGION ? + re `$SERVICE_NAME`.replace(AMAZON_AWS_HOST, SERVICE_NAME + "." + self.region) : AMAZON_AWS_HOST; string baseURL = HTTPS + self.amazonHost; - self.accessKeyId = config.accessKeyId; - self.secretAccessKey = config.secretAccessKey; - check verifyCredentials(self.accessKeyId, self.secretAccessKey); + self.authType = config.authType; + if self.authType is AWS_STATIC_AUTH { + self.accessKeyId = config.accessKeyId; + self.secretAccessKey = config.secretAccessKey; + self.sessionToken = (); + } else { + IAMCredentials iamCredentials = check getIAMCredentials(); + self.accessKeyId = iamCredentials.AccessKeyId; + self.secretAccessKey = iamCredentials.SecretAccessKey; + self.sessionToken = iamCredentials.Token; + } + check verifyCredentials(self.accessKeyId, self.secretAccessKey); http:ClientConfiguration httpClientConfig = check config:constructHTTPClientConfig(config); httpClientConfig.http1Settings = {chunking: http:CHUNKING_NEVER}; - self.amazonS3 = check new(baseURL, httpClientConfig); + httpClientConfig.followRedirects = {maxCount: 5, enabled: true, allowAuthHeaders: true}; + self.amazonS3 = check new (baseURL, httpClientConfig); } # Retrieves a list of all Amazon S3 buckets that the authenticated user of the request owns. - # + # # + return - If success, a list of Bucket record, else an error @display {label: "List Buckets"} remote isolated function listBuckets() returns @tainted Bucket[]|error { map requestHeaders = setDefaultHeaders(self.amazonHost); check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, GET, SLASH, UNSIGNED_PAYLOAD, - requestHeaders); + requestHeaders, sessionToken = self.sessionToken); http:Response httpResponse = check self.amazonS3->get(SLASH, requestHeaders); xml xmlPayload = check httpResponse.getXmlPayload(); if (httpResponse.statusCode == http:STATUS_OK) { @@ -69,13 +80,13 @@ public isolated client class Client { } # Creates a bucket. - # + # # + bucketName - A unique name for the bucket # + cannedACL - The access control list of the new bucket # + return - An error on failure or else `()` @display {label: "Create Bucket"} remote isolated function createBucket(@display {label: "Bucket Name"} string bucketName, - @display {label: "Access Control List"} CannedACL? cannedACL = ()) returns + @display {label: "Access Control List"} CannedACL? cannedACL = ()) returns @tainted error? { http:Request request = new; string requestURI = string `/${bucketName}/`; @@ -86,18 +97,18 @@ public isolated client class Client { if (self.region != DEFAULT_REGION) { xml xmlPayload = xml ` ${self.region} - `; + `; request.setXmlPayload(xmlPayload); } - + check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, PUT, requestURI, UNSIGNED_PAYLOAD, - requestHeaders, request); + requestHeaders, request, sessionToken = self.sessionToken); http:Response httpResponse = check self.amazonS3->put(requestURI, request); return handleHttpResponse(httpResponse); } # Retrieves the existing objects in a given bucket. - # + # # + bucketName - The name of the bucket # + delimiter - A delimiter is a character you use to group keys # + encodingType - The encoding method to be applied to the response @@ -105,34 +116,34 @@ public isolated client class Client { # + prefix - The prefix of the objects to be listed. If unspecified, all objects are listed # + startAfter - Object key from which to begin listing # + fetchOwner - Set to true, to retrieve the owner information in the response. By default the API does not return - # the Owner information in the response + # the Owner information in the response # + continuationToken - When the response to this API call is truncated (that is, the IsTruncated response element - # value is true), the response also includes the NextContinuationToken element. - # To list the next set of objects, you can use the NextContinuationToken element in the next - # request as the continuation-token + # value is true), the response also includes the NextContinuationToken element. + # To list the next set of objects, you can use the NextContinuationToken element in the next + # request as the continuation-token # + return - If success, list of S3 objects, else an error @display {label: "List Objects"} remote isolated function listObjects(@display {label: "Bucket Name"} string bucketName, - @display {label: "Group Identifier"} string? delimiter = (), - @display {label: "Encoding Type"} string? encodingType = (), - @display {label: "Maximum Number of Keys"} int? maxKeys = (), - @display {label: "Required Object Prefix"} string? prefix = (), - @display {label: "Object Key Starts From"} string? startAfter = (), - @display {label: "Is Owner Information Required?"} boolean? fetchOwner = (), - @display {label: "Next List Token"} string? continuationToken = ()) returns @tainted + @display {label: "Group Identifier"} string? delimiter = (), + @display {label: "Encoding Type"} string? encodingType = (), + @display {label: "Maximum Number of Keys"} int? maxKeys = (), + @display {label: "Required Object Prefix"} string? prefix = (), + @display {label: "Object Key Starts From"} string? startAfter = (), + @display {label: "Is Owner Information Required?"} boolean? fetchOwner = (), + @display {label: "Next List Token"} string? continuationToken = ()) returns @tainted @display {label: "List of Objects"} S3Object[]|error { map queryParamsMap = {}; string requestURI = string `/${bucketName}/`; string queryParamsStr = "?list-type=2"; queryParamsMap["list-type"] = "2"; - string queryParams = populateOptionalParameters(queryParamsMap, delimiter = delimiter, encodingType = - encodingType, maxKeys = maxKeys, prefix = prefix, startAfter = startAfter, fetchOwner = fetchOwner, - continuationToken = continuationToken); + string queryParams = populateOptionalParameters(queryParamsMap, delimiter = delimiter, encodingType = + encodingType, maxKeys = maxKeys, prefix = prefix, startAfter = startAfter, fetchOwner = fetchOwner, + continuationToken = continuationToken); queryParamsStr = string `${queryParamsStr}${queryParams}`; map requestHeaders = setDefaultHeaders(self.amazonHost); check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, GET, requestURI, UNSIGNED_PAYLOAD, - requestHeaders, queryParams = queryParamsMap); + requestHeaders, queryParams = queryParamsMap, sessionToken = self.sessionToken); requestURI = string `${requestURI}${queryParamsStr}`; http:Response httpResponse = check self.amazonS3->get(requestURI, requestHeaders); xml xmlPayload = check httpResponse.getXmlPayload(); @@ -151,19 +162,19 @@ public isolated client class Client { # + return - If success, S3ObjectContent object, else an error @display {label: "Get Object"} remote isolated function getObject(@display {label: "Bucket Name"} string bucketName, - @display {label: "Object Name"} string objectName, - @display {label: "Object Retrieval Headers"} ObjectRetrievalHeaders? - objectRetrievalHeaders = (), - @display {label: "Byte Array Size"} int? byteArraySize = ()) - returns @tainted @display {label: "Byte Stream"} stream|error { + @display {label: "Object Name"} string objectName, + @display {label: "Object Retrieval Headers"} ObjectRetrievalHeaders? + objectRetrievalHeaders = (), + @display {label: "Byte Array Size"} int? byteArraySize = ()) + returns @tainted@display {label: "Byte Stream"} stream|error { string requestURI = string `/${bucketName}/${objectName}`; map requestHeaders = setDefaultHeaders(self.amazonHost); // Add optional headers. populateGetObjectHeaders(requestHeaders, objectRetrievalHeaders); - + check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, GET, requestURI, UNSIGNED_PAYLOAD, - requestHeaders); + requestHeaders, sessionToken = self.sessionToken); http:Response httpResponse = check self.amazonS3->get(requestURI, requestHeaders); if (httpResponse.statusCode == http:STATUS_OK || (httpResponse.statusCode == http:STATUS_PARTIAL_CONTENT && objectRetrievalHeaders?.range != ())) { if byteArraySize is int { @@ -187,11 +198,11 @@ public isolated client class Client { # + return - An error on failure or else `()` @display {label: "Create Object"} remote isolated function createObject(@display {label: "Bucket Name"} string bucketName, - @display {label: "Object Name"} string objectName, - @display {label: "File Content"} string|xml|json|byte[]|stream payload, - @display {label: "Grant"} CannedACL? cannedACL = (), - @display {label: "Object Creation Headers"} ObjectCreationHeaders? objectCreationHeaders = (), - @display {label: "User Metadata Headers"} map userMetadataHeaders = {}) + @display {label: "Object Name"} string objectName, + @display {label: "File Content"} string|xml|json|byte[]|stream payload, + @display {label: "Grant"} CannedACL? cannedACL = (), + @display {label: "Object Creation Headers"} ObjectCreationHeaders? objectCreationHeaders = (), + @display {label: "User Metadata Headers"} map userMetadataHeaders = {}) returns error? { http:Request request = new; string requestURI = string `/${bucketName}/${objectName}`; @@ -203,29 +214,29 @@ public isolated client class Client { } else { request.setPayload(payload); } - + // Add optional headers. populateCreateObjectHeaders(requestHeaders, objectCreationHeaders); // Add user-defined metadata headers. populateUserMetadataHeaders(requestHeaders, userMetadataHeaders); - + check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, PUT, requestURI, UNSIGNED_PAYLOAD, - requestHeaders, request); + requestHeaders, request, sessionToken = self.sessionToken); http:Response httpResponse = check self.amazonS3->put(requestURI, request); return handleHttpResponse(httpResponse); } # Deletes an object. - # + # # + bucketName - The name of the bucket # + objectName - The name of the object # + versionId - The specific version of the object to delete, if versioning is enabled # + return - An error on failure or else `()` @display {label: "Delete Object"} remote isolated function deleteObject(@display {label: "Bucket Name"} string bucketName, - @display {label: "Object Name"} string objectName, - @display {label: "Object Version"} string? versionId = ()) + @display {label: "Object Name"} string objectName, + @display {label: "Object Version"} string? versionId = ()) returns @tainted error? { map queryParamsMap = {}; http:Request request = new; @@ -236,18 +247,18 @@ public isolated client class Client { if (versionId is string) { queryParamsStr = string `${queryParamsStr}?versionId=${versionId}`; queryParamsMap["versionId"] = versionId; - } + } map requestHeaders = setDefaultHeaders(self.amazonHost); - + check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, DELETE, requestURI, - UNSIGNED_PAYLOAD, requestHeaders, request, queryParams = queryParamsMap); + UNSIGNED_PAYLOAD, requestHeaders, request, queryParams = queryParamsMap, sessionToken = self.sessionToken); requestURI = string `${requestURI}${queryParamsStr}`; http:Response httpResponse = check self.amazonS3->delete(requestURI, request); return handleHttpResponse(httpResponse); - } + } # Deletes a bucket. - # + # # + bucketName - The name of the bucket # + return - An error on failure or else `()` @display {label: "Delete Bucket"} @@ -255,19 +266,19 @@ public isolated client class Client { http:Request request = new; string requestURI = string `/${bucketName}`; map requestHeaders = setDefaultHeaders(self.amazonHost); - + check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, DELETE, requestURI, - UNSIGNED_PAYLOAD, requestHeaders, request); + UNSIGNED_PAYLOAD, requestHeaders, request, sessionToken = self.sessionToken); http:Response httpResponse = check self.amazonS3->delete(requestURI, request); return handleHttpResponse(httpResponse); } - + # Generates a presigned URL for the object. # # + bucketName - The name of the bucket # + objectName - The name of the object # + action - The action to be done on the object (`RETRIEVE` for object retrieval or `CREATE` for object creation) - # or the relevant headers for object retrieval or creation + # or the relevant headers for object retrieval or creation # + expires - The time period for which the presigned URL is valid, in seconds # + partNo - The part number of the object, when uploading multipart objects # + uploadId - The upload ID of the multipart upload @@ -276,13 +287,13 @@ public isolated client class Client { remote isolated function createPresignedUrl( @display {label: "Bucket Name"} string bucketName, @display {label: "Object Name"} string objectName, - @display {label: "Object retrieval or creation indication with optional headers"} + @display {label: "Object retrieval or creation indication with optional headers"} ObjectAction|ObjectCreationHeaders|ObjectRetrievalHeaders action, @display {label: "Expiration Time"} int expires = 1800, @display {label: "Part Number"} int? partNo = (), @display {label: "Upload ID"} string? uploadId = ()) returns string|error { - + if expires < 0 { return error(EXPIRATION_TIME_ERROR_MSG); } @@ -296,7 +307,7 @@ public isolated client class Client { [string, string] [amzDateStr, shortDateStr] = check generateDateString(); map requestHeaders = { - [HOST] : self.amazonHost + [HOST]: self.amazonHost }; GET|PUT httpMethod; @@ -304,13 +315,13 @@ public isolated client class Client { if action is ObjectCreationHeaders { httpMethod = PUT; populateCreateObjectHeaders(requestHeaders, action); - } - + } + if action is ObjectRetrievalHeaders { httpMethod = GET; populateGetObjectHeaders(requestHeaders, action); - } - + } + if action is CREATE { httpMethod = PUT; } else { @@ -341,8 +352,8 @@ public isolated client class Client { string canonicalRequest = string `${httpMethod}${"\n"}/${bucketName}${string `/${objectName}`}${"\n"}${ canonicalQueryString}${"\n"}${canonicalHeaders}${"\n"}${signedHeaders}${"\n"}${UNSIGNED_PAYLOAD}`; string stringToSign = generateStringToSign(amzDateStr, shortDateStr, self.region, canonicalRequest); - string signature = check constructPresignedUrlSignature(self.accessKeyId, self.secretAccessKey, shortDateStr, - self.region, stringToSign); + string signature = check constructPresignedUrlSignature(self.accessKeyId, self.secretAccessKey, shortDateStr, + self.region, stringToSign); return string `${HTTPS}${self.amazonHost}/${bucketName}/${objectName}?${canonicalQueryString}&${X_AMZ_SIGNATURE }=${signature}`; } @@ -369,7 +380,7 @@ public isolated client class Client { } http:Request request = new; - + string requestURI = string `/${bucketName}/${objectName}`; string queryParamStr = string `?uploads`; map requestHeaders = setDefaultHeaders(self.amazonHost); @@ -379,7 +390,7 @@ public isolated client class Client { populateMultipartUploadHeaders(requestHeaders, multipartUploadHeaders); check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, POST, requestURI, UNSIGNED_PAYLOAD, - requestHeaders, request, queryParams = {"uploads": EMPTY_STRING}); + requestHeaders, request, queryParams = {"uploads": EMPTY_STRING}, sessionToken = self.sessionToken); requestURI = string `${requestURI}${queryParamStr}`; http:Response httpResponse = check self.amazonS3->post(requestURI, request); @@ -390,7 +401,7 @@ public isolated client class Client { return error(XMLPayload.toString()); } } - + # Completes a multipart upload by assembling previously uploaded parts. # # + objectName - The name of the object @@ -431,9 +442,9 @@ public isolated client class Client { } else { request.setPayload(payload); } - + check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, PUT, requestURI, UNSIGNED_PAYLOAD, - requestHeaders, request, queryParams = {"partNumber": partNumber.toString(), "uploadId": uploadId}); + requestHeaders, request, queryParams = {"partNumber": partNumber.toString(), "uploadId": uploadId}, sessionToken = self.sessionToken); requestURI = string `${requestURI}${queryParamStr}`; http:Response httpResponse = check self.amazonS3->put(requestURI, request); @@ -459,7 +470,7 @@ public isolated client class Client { @display {label: "Upload ID"} string uploadId, @display {label: "Completed Parts"} CompletedPart[] completedParts) returns error? { - + if objectName == EMPTY_STRING { return error(EMPTY_OBJECT_NAME_ERROR_MSG); } @@ -474,8 +485,8 @@ public isolated client class Client { map requestHeaders = setDefaultHeaders(self.amazonHost); - check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, POST, requestURI, - UNSIGNED_PAYLOAD, requestHeaders, request, queryParams = {"uploadId": uploadId}); + check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, POST, requestURI, + UNSIGNED_PAYLOAD, requestHeaders, request, queryParams = {"uploadId": uploadId}, sessionToken = self.sessionToken); requestURI = string `${requestURI}${queryParamStr}`; string payload = string ``; @@ -500,7 +511,7 @@ public isolated client class Client { @display {label: "Bucket Name"} string bucketName, @display {label: "Upload ID"} string uploadId) returns error? { - + if objectName == EMPTY_STRING { return error(EMPTY_OBJECT_NAME_ERROR_MSG); } @@ -513,9 +524,9 @@ public isolated client class Client { string requestURI = string `/${bucketName}/${objectName}`; map requestHeaders = setDefaultHeaders(self.amazonHost); - check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, DELETE, requestURI, - UNSIGNED_PAYLOAD, requestHeaders, request, queryParams = {"uploadId": uploadId}); - + check generateSignature(self.accessKeyId, self.secretAccessKey, self.region, DELETE, requestURI, + UNSIGNED_PAYLOAD, requestHeaders, request, queryParams = {"uploadId": uploadId}, sessionToken = self.sessionToken); + requestURI = string `${requestURI}?uploadId=${uploadId}`; http:Response httpResponse = check self.amazonS3->delete(requestURI, request); @@ -535,7 +546,7 @@ isolated function setDefaultHeaders(string amazonHost) returns map { # # + accessKeyId - The access key of the Amazon S3 account # + secretAccessKey - The secret access key of the Amazon S3 account -# +# # + return - An error on failure or else `()` isolated function verifyCredentials(string accessKeyId, string secretAccessKey) returns error? { if ((accessKeyId == "") || (secretAccessKey == "")) { diff --git a/s3/constants.bal b/s3/constants.bal index ea0234f..8fb9bad 100644 --- a/s3/constants.bal +++ b/s3/constants.bal @@ -38,6 +38,8 @@ const string X_AMZ_DATE = "X-Amz-Date"; const string HOST = "Host"; const string X_AMZ_ACL = "x-amz-acl"; const string X_AMZ_MFA = "x-amz-mfa"; +const string X_AWS_EC2_METADATA_TOKEN = "X-aws-ec2-metadata-token"; +const string X_AWS_EC2_METADATA_TOKEN_TTL_SECONDS = "X-aws-ec2-metadata-token-ttl-seconds"; const string CACHE_CONTROL = "Cache-Control"; const string CONTENT_DISPOSITION = "Content-Disposition"; const string CONTENT_ENCODING = "Content-Encoding"; @@ -50,12 +52,13 @@ const string IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; const string IF_MATCH = "If-Match"; const string IF_NONE_MATCH = "If-None-Match"; const string RANGE = "Range"; -const string AUTHORIZATION= "Authorization"; +const string AUTHORIZATION = "Authorization"; const X_AMZ_EXPIRES = "X-Amz-Expires"; const X_AMZ_ALGORITHM = "X-Amz-Algorithm"; const X_AMZ_CREDENTIAL = "X-Amz-Credential"; const X_AMZ_SIGNED_HEADERS = "X-Amz-SignedHeaders"; const X_AMZ_SIGNATURE = "X-Amz-Signature"; +const X_AMZ_SECURITY_TOKEN = "X-amz-security-token"; // HTTP verbs. const string GET = "GET"; @@ -70,6 +73,10 @@ const string AMAZON_AWS_HOST = "s3.amazonaws.com"; const string DEFAULT_REGION = "us-east-1"; const string ERROR_REASON_PREFIX = "{ballerinax/aws.s3}"; +# IAM role related constants. +const string METADATA_TOKEN_URL = "http://169.254.169.254/latest/api/token"; +const string METADATA_BASE_URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials"; + # Error messages. const string EMPTY_VALUES_FOR_CREDENTIALS_ERROR_MSG = "Empty values set for accessKeyId or secretAccessKey credential"; const string DATE_STRING_GENERATION_ERROR_MSG = "Error occured while generating date strings."; diff --git a/s3/records.bal b/s3/records.bal index 834f677..ecd50d9 100644 --- a/s3/records.bal +++ b/s3/records.bal @@ -25,32 +25,52 @@ public const ACL_AUTHENTICATED_READ = "aws-exec-read"; public const ACL_LOG_DELIVERY_WRITE = "authenticated-read"; public const ACL_BUCKET_OWNER_READ = "bucket-owner-read"; public const ACL_BUCKET_OWNER_FULL_CONTROL = "bucket-owner-full-control"; +public const AWS_STATIC_AUTH = "STATIC_AUTH"; +public const EC2_IAM_ROLE = "EC2_IAM_ROLE"; # Represents the AmazonS3 Connector configurations. # @display {label: "Connection Config"} public type ConnectionConfig record {| *config:ConnectionConfig; + # auth never auth?; # The access key of the Amazon S3 account - string accessKeyId; + string accessKeyId?; # The secret access key of the Amazon S3 account @display { label: "", kind: "password" } - string secretAccessKey; + string secretAccessKey?; # The AWS Region. If you don't specify an AWS region, Client uses US East (N. Virginia) as default region string region?; # The HTTP version understood by the client http:HttpVersion httpVersion = http:HTTP_1_1; + # The type of authentication to be used + AWS_STATIC_AUTH|EC2_IAM_ROLE authType = AWS_STATIC_AUTH; + # The session token for temporary credentials + string sessionToken?; |}; +# IAMCredentials required for the IAM role based authentication. +# +# + AccessKeyId - access key id +# + SecretAccessKey - Secret access key +# + Token - Token +# + Expiration - Expiration time +type IAMCredentials record { + string AccessKeyId; + string SecretAccessKey; + string Token; + string Expiration; +}; + public type CannedACL ACL_PRIVATE|ACL_PUBLIC_READ|ACL_PUBLIC_READ_WRITE|ACL_AUTHENTICATED_READ|ACL_LOG_DELIVERY_WRITE| ACL_BUCKET_OWNER_READ|ACL_BUCKET_OWNER_FULL_CONTROL; # Defines bucket. -# +# # + name - The name of the bucket # + creationDate - The creation date of the bucket public type Bucket record { @@ -59,7 +79,7 @@ public type Bucket record { }; # Define S3Object. -# +# # + objectName - The name of the object # + lastModified - The last modified date of the object # + eTag - The etag of the object @@ -151,7 +171,7 @@ public type GetHeaders record { # + cacheControl - Can be used to specify caching behavior along the request/reply chain # + contentDisposition - Specifies presentational information for the object # + contentEncoding - Specifies what content encodings have been applied to the object and thus what decoding mechanisms -# must be applied to obtain the media-type referenced by the Content-Type header field +# must be applied to obtain the media-type referenced by the Content-Type header field # + contentLanguage - The language the content is in # + contentType - The MIME type of the content # + expires - The date and time at which the object is no longer cacheable diff --git a/s3/utils.bal b/s3/utils.bal index 85c00c3..5eda5e6 100644 --- a/s3/utils.bal +++ b/s3/utils.bal @@ -25,12 +25,12 @@ import ballerina/url; isolated function generateSignature(string accessKeyId, string secretAccessKey, string region, string httpVerb, string requestURI, string payload, map headers, http:Request? request = (), - map? queryParams = ()) returns @tainted error? { + map? queryParams = (), string? sessionToken = ()) returns @tainted error? { string canonicalRequest = httpVerb; string canonicalQueryString = ""; string requestPayload = ""; map requestHeaders = headers; - [string, string][amzDateStr, shortDateStr] = ["",""]; + [string, string] [amzDateStr, shortDateStr] = ["", ""]; // Generate date strings and put it in the headers map to generate the signature. [string, string]|error dateStrings = generateDateString(); @@ -41,6 +41,11 @@ isolated function generateSignature(string accessKeyId, string secretAccessKey, requestHeaders[X_AMZ_DATE] = amzDateStr; } + // Add x-amz-security-token header if session token is provided. + if sessionToken is string { + requestHeaders[X_AMZ_SECURITY_TOKEN] = sessionToken; + } + // Get canonical URI. var canonicalURI = getCanonicalURI(requestURI); if (canonicalURI is string) { @@ -59,12 +64,12 @@ isolated function generateSignature(string accessKeyId, string secretAccessKey, requestPayload = payload; } else if (request is http:Request) { requestPayload = array:toBase16(crypto:hashSha256(payload.toBytes())).toLowerAscii(); - string contentType = check request.getHeader(CONTENT_TYPE.toLowerAscii()); + string contentType = check request.getHeader(CONTENT_TYPE.toLowerAscii()); requestHeaders[CONTENT_TYPE] = contentType; } // Generete canonical and signed headers. - [string, string] [canonicalHeaders,signedHeaders] = generateCanonicalHeaders(headers, request); + [string, string] [canonicalHeaders, signedHeaders] = generateCanonicalHeaders(headers, request); // Generate canonical request. canonicalRequest = string `${canonicalRequest}${"\n"}${canonicalURI}${"\n"}${canonicalQueryString}${"\n"}`; @@ -73,13 +78,19 @@ isolated function generateSignature(string accessKeyId, string secretAccessKey, // Generate string to sign. string stringToSign = generateStringToSign(amzDateStr, shortDateStr, region, canonicalRequest); // Construct authorization signature string. - string authHeader = check constructAuthSignature(accessKeyId, secretAccessKey, shortDateStr, region, - signedHeaders, stringToSign); + string authHeader = check constructAuthSignature(accessKeyId, secretAccessKey, shortDateStr, region, + signedHeaders, stringToSign, sessionToken); // Set authorization header. if (request is http:Request) { request.setHeader(AUTHORIZATION, authHeader); + if sessionToken is string { + request.setHeader(X_AMZ_SECURITY_TOKEN, sessionToken); + } } else { - requestHeaders[AUTHORIZATION] = authHeader; + requestHeaders[AUTHORIZATION] = authHeader; + if sessionToken is string { + requestHeaders[X_AMZ_SECURITY_TOKEN] = sessionToken; + } } } else { return error(CANONICAL_URI_GENERATION_ERROR_MSG, canonicalURI); @@ -97,7 +108,7 @@ isolated function generateDateString() returns [string, string]|error { } isolated function utcToString(time:Utc utc, string pattern) returns string|error { - [int, decimal][epochSeconds, lastSecondFraction] = utc; + [int, decimal] [epochSeconds, lastSecondFraction] = utc; int nanoAdjustments = (lastSecondFraction * 1000000000); var instant = ofEpochSecond(epochSeconds, nanoAdjustments); var zoneId = getZoneId(java:fromString("Z")); @@ -116,7 +127,7 @@ isolated function utcToString(time:Utc utc, string pattern) returns string|error # # + return - String to sign. isolated function generateStringToSign(string amzDateStr, string shortDateStr, string region, string canonicalRequest) - returns string{ + returns string { //Start creating the string to sign string stringToSign = string `${AWS4_HMAC_SHA256}${"\n"}${amzDateStr}${"\n"}${shortDateStr}/${region}/${SERVICE_NAME}/${TERMINATION_STRING}${"\n"}${array:toBase16(crypto:hashSha256(canonicalRequest.toBytes())).toLowerAscii()}`; return stringToSign; @@ -166,7 +177,7 @@ isolated function generateCanonicalQueryString(map queryParams) returns # + headers - Headers map. # + request - HTTP request. # + return - Return canonical and signed headers. -isolated function generateCanonicalHeaders(map headers, http:Request? request) returns @tainted[string, string] { +isolated function generateCanonicalHeaders(map headers, http:Request? request) returns @tainted [string, string] { string canonicalHeaders = ""; string signedHeaders = ""; string key; @@ -210,13 +221,18 @@ isolated function generateSigningKey(string secretAccessKey, string shortDateStr # + region - Endpoint region. # + signedHeaders - Signed headers. # + stringToSign - stringToSign Parameter Description +# + sessionToken - Optional session token. # + return - Authorization header string value. isolated function constructAuthSignature(string accessKeyId, string secretAccessKey, string shortDateStr, string region, - string signedHeaders, string stringToSign) returns string|error { + string signedHeaders, string stringToSign, string? sessionToken = ()) returns string|error { byte[] signingKey = check generateSigningKey(secretAccessKey, shortDateStr, region); string encodedStr = array:toBase16(check crypto:hmacSha256(stringToSign.toBytes(), signingKey)); string credential = string `${accessKeyId}/${shortDateStr}/${region}/${SERVICE_NAME}/${TERMINATION_STRING}`; - string authHeader = string `${AWS4_HMAC_SHA256} ${CREDENTIAL}=${credential},${SIGNED_HEADER}=${signedHeaders}`; + string sHeaders = signedHeaders; + if sessionToken is string { + sHeaders = string `${sHeaders};${X_AMZ_SECURITY_TOKEN}`; + } + string authHeader = string `${AWS4_HMAC_SHA256} ${CREDENTIAL}=${credential},${SIGNED_HEADER}=${sHeaders}`; authHeader = string `${authHeader},${SIGNATURE}=${encodedStr.toLowerAscii()}`; return authHeader; } @@ -229,7 +245,7 @@ isolated function constructAuthSignature(string accessKeyId, string secretAccess # + region - Endpoint region # + stringToSign - String including information such as the HTTP method, resource path, query parameters, and headers # + return - Signature used for authentication -isolated function constructPresignedUrlSignature(string accessKeyId, string secretAccessKey, string shortDateStr, +isolated function constructPresignedUrlSignature(string accessKeyId, string secretAccessKey, string shortDateStr, string region, string stringToSign) returns string|error { byte[] signingKey = check generateSigningKey(secretAccessKey, shortDateStr, region); string encodedStr = array:toBase16(check crypto:hmacSha256(stringToSign.toBytes(), signingKey)); @@ -242,7 +258,7 @@ isolated function constructPresignedUrlSignature(string accessKeyId, string secr # + objectCreationHeaders - Optional headers for createObject function. isolated function populateCreateObjectHeaders(map requestHeaders, ObjectCreationHeaders? objectCreationHeaders) { - if(objectCreationHeaders != ()) { + if (objectCreationHeaders != ()) { if (objectCreationHeaders?.cacheControl != ()) { requestHeaders[CACHE_CONTROL] = objectCreationHeaders?.cacheControl; } @@ -274,7 +290,7 @@ isolated function populateCreateObjectHeaders(map requestHeaders, Object # # + requestHeaders - Request headers map. # + userMetadataHeaders - Map containing user-defined metadata. -isolated function populateUserMetadataHeaders(map requestHeaders, map userMetadataHeaders){ +isolated function populateUserMetadataHeaders(map requestHeaders, map userMetadataHeaders) { foreach string metadataKey in userMetadataHeaders.keys() { requestHeaders[string `x-amz-meta-${metadataKey.toLowerAscii()}`] = userMetadataHeaders.get(metadataKey); } @@ -285,7 +301,7 @@ isolated function populateUserMetadataHeaders(map requestHeaders, map requestHeaders, ObjectRetrievalHeaders? objectRetrievalHeaders) { - if(objectRetrievalHeaders != ()) { + if (objectRetrievalHeaders != ()) { if (objectRetrievalHeaders?.modifiedSince != ()) { requestHeaders[IF_MODIFIED_SINCE] = objectRetrievalHeaders?.modifiedSince; } @@ -305,12 +321,12 @@ isolated function populateGetObjectHeaders(map requestHeaders, ObjectRet } isolated function populateOptionalParameters(map queryParamsMap, string? delimiter = (), string? encodingType - = (), int? maxKeys = (), string? prefix = (), string? startAfter = (), - boolean? fetchOwner = (), string? continuationToken = ()) returns + = (), int? maxKeys = (), string? prefix = (), string? startAfter = (), + boolean? fetchOwner = (), string? continuationToken = ()) returns string { string queryParamsStr = ""; // Append query parameter(delimiter). - if (delimiter is string) { + if (delimiter is string) { queryParamsStr = string `${queryParamsStr}&delimiter=${delimiter}`; queryParamsMap["delimiter"] = delimiter; } @@ -407,3 +423,28 @@ isolated function handleHttpResponse(http:Response httpResponse) returns @tainte return error(xmlPayload.toString()); } } + +isolated function getIAMCredentials() returns IAMCredentials|error { + // Create HTTP client for metadata service + http:Client metadataClient = check new (METADATA_TOKEN_URL); + + // Get IMDSv2 token + string token = check metadataClient->put("", {}, {[X_AWS_EC2_METADATA_TOKEN_TTL_SECONDS]: "21600"}); + + // Get role name + http:Client metaDataRoleClient = check new (METADATA_BASE_URL); + string roleName = check metaDataRoleClient->/( + headers = { + [X_AWS_EC2_METADATA_TOKEN]: token + } + ); + + // Get credentials + string credResult = check metaDataRoleClient->/[roleName]( + headers = { + [X_AWS_EC2_METADATA_TOKEN]: token + } + ); + return credResult.fromJsonStringWithType(); + +}