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

Add support for Event Hub SAS Helper #105

Merged
merged 2 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions eventhub/sas_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package eventhub

import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"strconv"
"strings"
"time"
)

const (
connStringSharedAccessKeyKey = "SharedAccessKey"
connStringSharedAccessKeyNameKey = "SharedAccessKeyName"
)

func ComputeEventHubSASToken(sharedAccessKeyName string,
sharedAccessKey string,
eventHubUri string,
expiry string,
) (string, error) {
uri := url.QueryEscape(eventHubUri)

expireTime, err := time.Parse(time.RFC3339, expiry)
if err != nil {
return "", err
}
expireTimestamp := expireTime.Unix()
expireStr := strconv.FormatInt(expireTimestamp, 10)

stringToSign := uri + "\n" + expireStr

key := []byte(sharedAccessKey)
h := hmac.New(sha256.New, key)
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))

sasToken := "sr=" + uri
sasToken += "&sig=" + url.QueryEscape(signature)
sasToken += "&se=" + (expireStr)
sasToken += "&skn=" + (sharedAccessKeyName)

return sasToken, nil
}

func ComputeEventHubSASConnectionString(sasToken string) string {
return fmt.Sprintf("SharedAccessSignature %s", sasToken)
}

func ComputeEventHubSASConnectionUrl(endpoint string, entityPath string) (*string, error) {
if endpoint == "" {
return nil, fmt.Errorf("endpoint cannot be empty")
}

var url string
if entityPath == "" {
url = strings.TrimRight(endpoint, "/")
} else {
url = endpoint + entityPath
}

return &url, nil
}

func ParseEventHubSASConnectionString(connString string) (map[string]string, error) {
// This connection string was for a real Event Hub which has been deleted
// so its safe to include here for reference to understand the format.
// Endpoint=sb://example-ehn.servicebus.windows.net/;SharedAccessKeyName=example-ehar;SharedAccessKey=DzGpfdyJda9D/xIkME0FLA66wZnheOBID0s1/rrtlHg=;EntityPath=example-eh
validKeys := map[string]bool{"Endpoint": true, "SharedAccessKeyName": true, "SharedAccessKey": true, "EntityPath": true}
// The k-v pairs are separated with semi-colons
tokens := strings.Split(connString, ";")

kvp := make(map[string]string)

for _, atoken := range tokens {
// The individual k-v are separated by an equals sign.
kv := strings.SplitN(atoken, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("[ERROR] token `%s` is an invalid key=pair (connection string %s)", atoken, connString)
}

key := kv[0]
val := kv[1]

if _, present := validKeys[key]; !present {
return nil, fmt.Errorf("[ERROR] Unknown Key `%s` in connection string %s", key, connString)
}
kvp[key] = val
}

if _, present := kvp[connStringSharedAccessKeyKey]; !present {
return nil, fmt.Errorf("[ERROR] Shared Access Key not found in connection string: %s", connString)
}

return kvp, nil
}
144 changes: 144 additions & 0 deletions eventhub/sas_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package eventhub

import (
"strings"
"testing"
)

func TestParseEventHubConnectionString(t *testing.T) {
testCases := []struct {
input string
expectedSharedAccessKeyName string
expectedSharedAccessKey string
expectedError bool
}{
{
"Endpoint=sb://acctesteventhubnamespace-test01.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=IUSvXLiPZ3uAQcso/cL7vTiL4zsc/EMtcUzNCC2dhaM=",
"RootManageSharedAccessKey",
"IUSvXLiPZ3uAQcso/cL7vTiL4zsc/EMtcUzNCC2dhaM=",
false,
},
{
"Endpoint=sb://acctesteventhubnamespace-test01.servicebus.windows.net/;SharedAccessKeyName=acctest-test01;SharedAccessKey=R9v9VaHiU/ktFIka8Q4aUbQnZeiSKaevncrsxOTTILw=;EntityPath=acctesteventhub-test01",
"acctest-test01",
"R9v9VaHiU/ktFIka8Q4aUbQnZeiSKaevncrsxOTTILw=",
false,
},
{
"Endpoint=sb://acctesteventhubnamespace-test01.servicebus.windows.net/;SharedAccessKeyName=acctest-test01;SharedAccessKey=R9v9VaHiU/ktFIka8Q4aUbQnZeiSKaevncrsxOTTILw=;EntityPath",
"",
"",
true,
},
}

for _, test := range testCases {
result, err := ParseEventHubSASConnectionString(test.input)

if test.expectedError {
if err == nil {
t.Fatalf("Expected error for %s: %q", test.input, err)
}
return
}

if !test.expectedError && err != nil {
t.Fatalf("Failed to parse resource type string: %s, %q", test.input, result)
}

if val, pres := result[connStringSharedAccessKeyKey]; !pres || val != test.expectedSharedAccessKey {
t.Fatalf("Failed to parse Shared Access Key: Expected: %s, Found: %s", test.expectedSharedAccessKey, val)
}
if val, pres := result[connStringSharedAccessKeyNameKey]; !pres || val != test.expectedSharedAccessKeyName {
t.Fatalf("Failed to parse Shared Access Name: Expected: %s, Found: %s", test.expectedSharedAccessKeyName, val)
}
}
}

func TestComputeEventHubSASToken(t *testing.T) {
testCases := []struct {
sharedAccessKeyName string
sharedAccessKey string
eventHubUri string
expiry string
knownSasToken string
}{
{
"RootManageSharedAccessKey",
"IUSvXLiPZ3uAQcso/cL7vTiL4zsc/EMtcUzNCC2dhaM=",
"sb://acctesteventhubnamespace-test01.servicebus.windows.net",
"2022-01-11T08:24:49Z",
"sr=sb%3A%2F%2Facctesteventhubnamespace-test01.servicebus.windows.net&sig=8dgxKVwLsOWxX7f4mNtyiez47lxYCJ8h%2FeViD%2BMWY2E%3D&se=1641889489&skn=RootManageSharedAccessKey",
},
{
"acctest-test01",
"R9v9VaHiU/ktFIka8Q4aUbQnZeiSKaevncrsxOTTILw=",
"sb://acctesteventhubnamespace-test01.servicebus.windows.net/acctesteventhub-test01",
"2022-01-11T08:24:49Z",
"sr=sb%3A%2F%2Facctesteventhubnamespace-test01.servicebus.windows.net%2Facctesteventhub-test01&sig=%2FaPTDsDZhwpdysw1klgV1fm5a%2Bo3vw2Lb7HsDHyZr4M%3D&se=1641889489&skn=acctest-test01",
},
}

for _, test := range testCases {
computedToken, err := ComputeEventHubSASToken(test.sharedAccessKeyName,
test.sharedAccessKey,
test.eventHubUri,
test.expiry)

if err != nil {
t.Fatalf("Test Failed: Error computing Event Hub Sas: %q", err)
}

if computedToken != test.knownSasToken {
t.Fatalf("Test failed: Expected Azure SAS %s but was %s", test.knownSasToken, computedToken)
}
}
}

func TestComputeEventHubSASConnectionString(t *testing.T) {
testCases := []struct {
sasToken string
sasConnectionString string
}{
{
"sr=sb%3A%2F%2Facctest-ehn-test01.servicebus.windows.net%2Facctest-eh-test01&sig=ozpLwoOHPAWD1s4GE2Khhu508JbcVA4%2FWutXZIV7VfI%3D&se=1672531200&skn=acctest-ehar-test01",
"SharedAccessSignature sr=sb%3A%2F%2Facctest-ehn-test01.servicebus.windows.net%2Facctest-eh-test01&sig=ozpLwoOHPAWD1s4GE2Khhu508JbcVA4%2FWutXZIV7VfI%3D&se=1672531200&skn=acctest-ehar-test01",
},
}

for _, test := range testCases {
computedConnectionString := ComputeEventHubSASConnectionString(test.sasToken)

if computedConnectionString != test.sasConnectionString {
t.Fatalf("Test failed: Expected SAS connection string is %s but was %s", computedConnectionString, test.sasConnectionString)
}
}
}

func TestComputeEventHubSASConnectionUrl(t *testing.T) {
testCases := []struct {
endpoint string
entityPath string
eventHubConnectionUrl string
}{
{
"sb://acctesteventhubnamespace-test01.servicebus.windows.net/",
"acctesteventhub-test01",
"sb://acctesteventhubnamespace-test01.servicebus.windows.net/acctesteventhub-test01",
},
{
"sb://acctesteventhubnamespace-test01.servicebus.windows.net/",
"",
"sb://acctesteventhubnamespace-test01.servicebus.windows.net",
},
}

for _, test := range testCases {
computedEventHubConnectionUrl, err := ComputeEventHubSASConnectionUrl(test.endpoint, test.entityPath)
if err != nil {
t.Fatalf("Test failed: This call should not have thrown an error")
} else if strings.Compare(*computedEventHubConnectionUrl, test.eventHubConnectionUrl) != 0 {
t.Fatalf("Test failed: Expected connection url is %s but was %s", *computedEventHubConnectionUrl, test.eventHubConnectionUrl)
}
}
}