Skip to content

Commit

Permalink
feat(go): add GetSecuredApiKeyRemainingValidity helper, fix `Genera…
Browse files Browse the repository at this point in the history
…teSecuredApiKey` encoding (#2934)
  • Loading branch information
shortcuts authored Mar 27, 2024
1 parent a4a8823 commit 21ebc89
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 34 deletions.
1 change: 1 addition & 0 deletions templates/go/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
{{#isSearchClient}}
"slices"
"time"
"sort"
"github.com/algolia/algoliasearch-client-go/v4/algolia/errs"
"crypto/hmac"
"crypto/sha256"
Expand Down
102 changes: 69 additions & 33 deletions templates/go/search_helpers.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -294,16 +294,64 @@ func (c *APIClient) WaitForApiKeyWithContext(
)
}

func encodeRestrictions(restrictions *SecuredAPIKeyRestrictions) (string, error) {
if restrictions == nil {
return "", nil
}

toSerialize := map[string]any{}
if restrictions.Filters != nil {
toSerialize["filters"] = *restrictions.Filters
}
if restrictions.ValidUntil != nil {
toSerialize["validUntil"] = *restrictions.ValidUntil
}
if restrictions.RestrictIndices != nil {
toSerialize["restrictIndices"] = restrictions.RestrictIndices
}
if restrictions.RestrictSources != nil {
toSerialize["restrictSources"] = *restrictions.RestrictSources
}
if restrictions.UserToken != nil {
toSerialize["userToken"] = *restrictions.UserToken
}
if restrictions.SearchParams != nil {
// merge with searchParams
serializedParams, err := restrictions.SearchParams.MarshalJSON()
if err != nil {
return "", fmt.Errorf("failed to marshal SearchParams: %w", err)
}
err = json.Unmarshal(serializedParams, &toSerialize)
if err != nil {
return "", fmt.Errorf("failed to unmarshal SearchParams: %w", err)
}
}

// sort the keys to ensure consistent encoding
keys := make([]string, 0, len(toSerialize))
for k := range toSerialize {
keys = append(keys, k)
}
sort.Strings(keys)

queryString := make([]string, 0, len(toSerialize))
for _, k := range keys {
queryString = append(queryString, k+"="+queryParameterToString(toSerialize[k]))
}

return strings.Join(queryString, "&"), nil
}

// GenerateSecuredApiKey generates a public API key intended to restrict access
// to certain records. This new key is built upon the existing key named
// `parentApiKey` and the following options:
// `parentApiKey` and the following options.
func (c *APIClient) GenerateSecuredApiKey(parentApiKey string, restrictions *SecuredAPIKeyRestrictions) (string, error) {
h := hmac.New(sha256.New, []byte(parentApiKey))
message, err := encodeRestrictions(restrictions)
if err != nil {
return "", err
}
if err != nil {
return "", err
}
_, err = h.Write([]byte(message))
if err != nil {
return "", fmt.Errorf("failed to compute HMAC: %w", err)
Expand All @@ -315,39 +363,27 @@ func (c *APIClient) GenerateSecuredApiKey(parentApiKey string, restrictions *Sec
return key, nil
}

func encodeRestrictions(restrictions *SecuredAPIKeyRestrictions) (string, error) {
toSerialize := map[string]any{}
if restrictions.Filters != nil {
toSerialize["filters"] = *restrictions.Filters
}
if restrictions.ValidUntil != nil {
toSerialize["validUntil"] = *restrictions.ValidUntil
}
if restrictions.RestrictIndices != nil {
toSerialize["restrictIndices"] = restrictions.RestrictIndices
// GetSecuredApiKeyRemainingValidity retrieves the remaining validity of the previously generated `securedApiKey`, the `ValidUntil` parameter must have been provided.
func (c *APIClient) GetSecuredApiKeyRemainingValidity(securedApiKey string) (time.Duration, error) {
if len(securedApiKey) == 0 {
return 0, fmt.Errorf("given secured API key is empty: %s", securedApiKey)
}
if restrictions.RestrictSources != nil {
toSerialize["restrictSources"] = *restrictions.RestrictSources

decoded, err := base64.StdEncoding.DecodeString(securedApiKey)
if err != nil {
return 0, fmt.Errorf("unable to decode given secured API key: %s", err)
}
if restrictions.UserToken != nil {
toSerialize["userToken"] = *restrictions.UserToken

submatch := regexp.MustCompile(`validUntil=(\d{1,10})`).FindSubmatch(decoded)

if len(submatch) != 2 {
return 0, fmt.Errorf("unable to find `validUntil` parameter in the given secured API key: %s", string(decoded))
}
if restrictions.SearchParams != nil {
// merge with searchParams
serializedParams, err := restrictions.SearchParams.MarshalJSON()
if err != nil {
return "", fmt.Errorf("failed to marshal SearchParams: %w", err)
}
err = json.Unmarshal(serializedParams, &toSerialize)
if err != nil {
return "", fmt.Errorf("failed to unmarshal SearchParams: %w", err)
}
}

queryString := make([]string, 0, len(toSerialize))
for k, v := range toSerialize {
queryString = append(queryString, k+"="+queryParameterToString(v))
ts, err := strconv.Atoi(string(submatch[1]))
if err != nil {
return 0, fmt.Errorf("invalid format for the received `validUntil` value: %s", string(submatch[1]))
}

return strings.Join(queryString, "&"), nil
return time.Until(time.Unix(int64(ts), 0)), nil
}
35 changes: 35 additions & 0 deletions templates/go/tests/requests/helpers.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
func TestSearch_GenerateSecuredApiKey(t *testing.T) {
client, echo := createSearchClient(t)
_ = echo
t.Run("generates a key without restrictions", func(t *testing.T) {
key, err := client.GenerateSecuredApiKey("foo", nil)
require.NoError(t, err)
require.Equal(t, "NjgzNzE2ZDlkN2Y4MmVlZDE3NGM2Y2FlYmUwODZlZTkzMzc2Yzc5ZDdjNjFkZDY3MGVhMDBmN2Y4ZDZlYjBhOA==", key)
})

t.Run("generates a key with restrictions", func(t *testing.T) {
key, err := client.GenerateSecuredApiKey("foo", search.NewSecuredAPIKeyRestrictions().SetValidUntil(100).SetRestrictIndices([]string{"bar"}).SetRestrictSources("192,168.1.0/24").SetUserToken("foobarbaz").SetSearchParams(search.NewSearchParamsObject().SetQuery("foo")))
require.NoError(t, err)

require.Equal(t, "NGMxODk0MjViNjM3ODcxNjc4NWU4Y2I5NGIxNDAzMTg4MjU5Mjc4YTEwMzU4Mjk2YjBiMmVjOWViYTIyOTBiY3F1ZXJ5PWZvbyZyZXN0cmljdEluZGljZXM9YmFyJnJlc3RyaWN0U291cmNlcz0xOTIlMkMxNjguMS4wJTJGMjQmdXNlclRva2VuPWZvb2JhcmJheiZ2YWxpZFVudGlsPTEwMA==", key)
})
}

func TestSearch_GetSecuredApiKeyRemainingVaildity(t *testing.T) {
client, echo := createSearchClient(t)
_ = echo
t.Run("is able to check the remaining validity of a key", func(t *testing.T) {
key, err := client.GenerateSecuredApiKey("foo", search.NewSecuredAPIKeyRestrictions().SetValidUntil(42))
require.NoError(t, err)
require.Equal(t, "NDI5ZjRkMTRiNTBlZmExZWIyN2I3NzczOGUwMzE0NjYwMDU1M2M3NjYyY2IxODZhMDAxMWEwOWJmZjE5MzY0NnZhbGlkVW50aWw9NDI=", key)
validity, err := client.GetSecuredApiKeyRemainingValidity(key)
require.NoError(t, err)
require.Greater(t, validity, -time.Now().UnixNano())
})
}
7 changes: 6 additions & 1 deletion templates/go/tests/requests/requests.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/url"
"os"
"testing"
"time"

"github.com/kinbiko/jsonassert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -135,4 +136,8 @@ func Test{{#lambda.titlecase}}{{clientPrefix}}{{/lambda.titlecase}}_{{#lambda.ti
{{/tests}}
}

{{/blocksRequests}}
{{/blocksRequests}}

{{#isSearchClient}}
{{> tests/requests/helpers}}
{{/isSearchClient}}

1 comment on commit 21ebc89

@github-actions
Copy link

Choose a reason for hiding this comment

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

Please sign in to comment.