Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New data source: azurerm_storage_account_blob_container_sas #4195

Merged
Merged
206 changes: 206 additions & 0 deletions azurerm/data_source_storage_account_blob_container_sas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package azurerm

import (
"crypto/sha256"
"encoding/hex"

"github.com/hashicorp/go-azure-helpers/storage"
"github.com/hashicorp/terraform/helper/schema"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate"
)

func dataSourceArmStorageAccountBlobContainerSharedAccessSignature() *schema.Resource {
return &schema.Resource{
Read: dataSourceArmStorageContainerSasRead,

Schema: map[string]*schema.Schema{
"connection_string": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
ValidateFunc: validate.NoEmptyStrings,
},

"container_name": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validate.NoEmptyStrings,
},

"https_only": {
Type: schema.TypeBool,
Optional: true,
Default: true,
},

"ip": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 on reflection could we make this ip_address to match the other resources?

Type: schema.TypeString,
Optional: true,
ValidateFunc: validate.SharedAccessSignatureIP,
},

"start": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validate.ISO8601DateTime,
},

"expiry": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validate.ISO8601DateTime,
},

"permissions": {
Type: schema.TypeList,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"read": {
Type: schema.TypeBool,
Required: true,
},

"add": {
Type: schema.TypeBool,
Required: true,
},

"create": {
Type: schema.TypeBool,
Required: true,
},

"write": {
Type: schema.TypeBool,
Required: true,
},

"delete": {
Type: schema.TypeBool,
Required: true,
},

"list": {
Type: schema.TypeBool,
Required: true,
},
},
},
},

"cache_control": {
Type: schema.TypeString,
Optional: true,
},

"content_disposition": {
Type: schema.TypeString,
Optional: true,
},

"content_encoding": {
Type: schema.TypeString,
Optional: true,
},

"content_language": {
Type: schema.TypeString,
Optional: true,
},

"content_type": {
Type: schema.TypeString,
Optional: true,
},

"sas": {
Type: schema.TypeString,
Computed: true,
Sensitive: true,
},
},
}

}

func dataSourceArmStorageContainerSasRead(d *schema.ResourceData, _ interface{}) error {

connString := d.Get("connection_string").(string)
containerName := d.Get("container_name").(string)
httpsOnly := d.Get("https_only").(bool)
ip := d.Get("ip").(string)
start := d.Get("start").(string)
expiry := d.Get("expiry").(string)
permissionsIface := d.Get("permissions").([]interface{})

// response headers
cacheControl := d.Get("cache_control").(string)
contentDisposition := d.Get("content_disposition").(string)
contentEncoding := d.Get("content_encoding").(string)
contentLanguage := d.Get("content_language").(string)
contentType := d.Get("content_type").(string)

permissions := buildContainerPermissionsString(permissionsIface[0].(map[string]interface{}))

// Parse the connection string
kvp, err := storage.ParseAccountSASConnectionString(connString)
if err != nil {
return err
}

// Create the string to sign with the key...
accountName := kvp[connStringAccountNameKey]
accountKey := kvp[connStringAccountKeyKey]
var signedProtocol = "https,http"
if httpsOnly {
signedProtocol = "https"
}
signedIp := ip
signedIdentifier := ""
signedSnapshotTime := ""

sasToken, err := storage.ComputeContainerSASToken(permissions, start, expiry, accountName, accountKey,
containerName, signedIdentifier, signedIp, signedProtocol, signedSnapshotTime, cacheControl,
contentDisposition, contentEncoding, contentLanguage, contentType)
if err != nil {
return err
}

d.Set("sas", sasToken)
tokenHash := sha256.Sum256([]byte(sasToken))
d.SetId(hex.EncodeToString(tokenHash[:]))

return nil
}

func buildContainerPermissionsString(perms map[string]interface{}) string {
retVal := ""

if val, pres := perms["read"].(bool); pres && val {
retVal += "r"
}

if val, pres := perms["add"].(bool); pres && val {
retVal += "a"
}

if val, pres := perms["create"].(bool); pres && val {
retVal += "c"
}

if val, pres := perms["write"].(bool); pres && val {
retVal += "w"
}

if val, pres := perms["delete"].(bool); pres && val {
retVal += "d"
}

if val, pres := perms["list"].(bool); pres && val {
retVal += "l"
}

return retVal
}
123 changes: 123 additions & 0 deletions azurerm/data_source_storage_account_blob_container_sas_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package azurerm

import (
"fmt"
"testing"
"time"

"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf"
)

func TestAccDataSourceArmStorageAccountBlobContainerSas_basic(t *testing.T) {
dataSourceName := "data.azurerm_storage_account_blob_container_sas.test"
rInt := tf.AccRandTimeInt()
rString := acctest.RandString(4)
location := testLocation()
utcNow := time.Now().UTC()
startDate := utcNow.Format(time.RFC3339)
endDate := utcNow.Add(time.Hour * 24).Format(time.RFC3339)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccDataSourceAzureRMStorageAccountBlobContainerSas_basic(rInt, rString, location, startDate, endDate),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, "https_only", "true"),
resource.TestCheckResourceAttr(dataSourceName, "start", startDate),
resource.TestCheckResourceAttr(dataSourceName, "expiry", endDate),
resource.TestCheckResourceAttr(dataSourceName, "ip", "168.1.5.65"),
resource.TestCheckResourceAttr(dataSourceName, "permissions.#", "1"),
resource.TestCheckResourceAttr(dataSourceName, "permissions.0.read", "true"),
resource.TestCheckResourceAttr(dataSourceName, "permissions.0.add", "true"),
resource.TestCheckResourceAttr(dataSourceName, "permissions.0.create", "false"),
resource.TestCheckResourceAttr(dataSourceName, "permissions.0.write", "false"),
resource.TestCheckResourceAttr(dataSourceName, "permissions.0.delete", "true"),
resource.TestCheckResourceAttr(dataSourceName, "permissions.0.list", "true"),
resource.TestCheckResourceAttr(dataSourceName, "cache_control", "max-age=5"),
resource.TestCheckResourceAttr(dataSourceName, "content_disposition", "inline"),
resource.TestCheckResourceAttr(dataSourceName, "content_encoding", "deflate"),
resource.TestCheckResourceAttr(dataSourceName, "content_language", "en-US"),
resource.TestCheckResourceAttr(dataSourceName, "content_type", "application/json"),
resource.TestCheckResourceAttrSet(dataSourceName, "sas"),
),
},
},
})
}

func testAccDataSourceAzureRMStorageAccountBlobContainerSas_basic(rInt int, rString string, location string, startDate string, endDate string) string {
return fmt.Sprintf(`
resource "azurerm_resource_group" "rg" {
name = "acctestsa-%d"
location = "%s"
}

resource "azurerm_storage_account" "storage" {
name = "acctestsads%s"
resource_group_name = "${azurerm_resource_group.rg.name}"

location = "${azurerm_resource_group.rg.location}"
account_tier = "Standard"
account_replication_type = "LRS"
}

resource "azurerm_storage_container" "container" {
name = "sas-test"
resource_group_name = "${azurerm_resource_group.rg.name}"
storage_account_name = "${azurerm_storage_account.storage.name}"
container_access_type = "private"
}

data "azurerm_storage_account_blob_container_sas" "test" {
connection_string = "${azurerm_storage_account.storage.primary_connection_string}"
container_name = "${azurerm_storage_container.container.name}"
https_only = true

ip = "168.1.5.65"

start = "%s"
expiry = "%s"

permissions {
read = true
add = true
create = false
write = false
delete = true
list = true
}

cache_control = "max-age=5"
content_disposition = "inline"
content_encoding = "deflate"
content_language = "en-US"
content_type = "application/json"
}
`, rInt, location, rString, startDate, endDate)
}

func TestAccDataSourceArmStorageAccountBlobContainerSas_permissionsString(t *testing.T) {
testCases := []struct {
input map[string]interface{}
expected string
}{
{map[string]interface{}{"read": true}, "r"},
{map[string]interface{}{"add": true}, "a"},
{map[string]interface{}{"create": true}, "c"},
{map[string]interface{}{"write": true}, "w"},
{map[string]interface{}{"delete": true}, "d"},
{map[string]interface{}{"list": true}, "l"},
{map[string]interface{}{"add": true, "write": true, "read": true, "delete": true}, "rawd"},
}

for _, test := range testCases {
result := buildContainerPermissionsString(test.input)
if test.expected != result {
t.Fatalf("Failed to build resource type string: expected: %s, result: %s", test.expected, result)
}
}
}
32 changes: 32 additions & 0 deletions azurerm/helpers/validate/shared_access_signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package validate

import (
"fmt"
"net"
"strings"
)

func SharedAccessSignatureIP(v interface{}, k string) (warnings []string, errors []error) {
value := v.(string)

if net.ParseIP(value) != nil {
return warnings, errors
}

ipRange := strings.Split(value, "-")

if len(ipRange) != 2 || net.ParseIP(ipRange[0]) == nil || net.ParseIP(ipRange[1]) == nil {
errors = append(errors, fmt.Errorf("%q must be a valid ipv4 address or a range of ipv4 addresses separated by a hyphen", k))
return warnings, errors
}

ip1 := ipRange[0]
ip2 := ipRange[1]

if ip1 == ip2 {
errors = append(errors, fmt.Errorf("IP addresses in a range for %q must be not be identical", k))
return warnings, errors
}

return warnings, errors
}