Skip to content

Commit

Permalink
feat(config): allow specifying api url with port and path
Browse files Browse the repository at this point in the history
deprecate ApiScheme, ApiHost and replace them with ApiUrl

close #56
  • Loading branch information
rhamzeh committed Nov 30, 2023
1 parent 25b9212 commit f6bcada
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 62 deletions.
27 changes: 12 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,8 @@ import (

func main() {
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiScheme: os.Getenv("OPENFGA_API_SCHEME"), // optional, defaults to "https"
ApiHost: os.Getenv("OPENFGA_API_HOST"), // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
StoreId: os.Getenv("OPENFGA_STORE_ID"), // not needed when calling `CreateStore` or `ListStores`
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. https://api.fga.example
StoreId: os.Getenv("FGA_STORE_ID"), // not needed when calling `CreateStore` or `ListStores`
})

if err != nil {
Expand All @@ -133,13 +132,12 @@ import (

func main() {
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiScheme: os.Getenv("OPENFGA_API_SCHEME"), // optional, defaults to "https"
ApiHost: os.Getenv("OPENFGA_API_HOST"), // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
StoreId: os.Getenv("OPENFGA_STORE_ID"), // not needed when calling `CreateStore` or `ListStores`
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. https://api.fga.example
StoreId: os.Getenv("FGA_STORE_ID"), // not needed when calling `CreateStore` or `ListStores`
Credentials: &credentials.Credentials{
Method: credentials.CredentialsMethodApiToken,
Config: &credentials.Config{
ApiToken: os.Getenv("OPENFGA_API_TOKEN"), // will be passed as the "Authorization: Bearer ${ApiToken}" request header
ApiToken: os.Getenv("FGA_API_TOKEN"), // will be passed as the "Authorization: Bearer ${ApiToken}" request header
},
},
})
Expand All @@ -162,17 +160,16 @@ import (

func main() {
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiScheme: os.Getenv("OPENFGA_API_SCHEME"), // optional, defaults to "https"
ApiHost: os.Getenv("OPENFGA_API_HOST"), // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
StoreId: os.Getenv("OPENFGA_STORE_ID"), // not needed when calling `CreateStore` or `ListStores`
AuthorizationModelId: openfga.PtrString("OPENFGA_AUTHORIZATION_MODEL_ID"),
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. https://api.fga.example
StoreId: os.Getenv("FGA_STORE_ID"), // not needed when calling `CreateStore` or `ListStores`
AuthorizationModelId: openfga.PtrString("FGA_AUTHORIZATION_MODEL_ID"),
Credentials: &credentials.Credentials{
Method: credentials.CredentialsMethodClientCredentials,
Config: &credentials.Config{
ClientCredentialsClientId: os.Getenv("OPENFGA_CLIENT_ID"),
ClientCredentialsClientSecret: os.Getenv("OPENFGA_CLIENT_SECRET"),
ClientCredentialsApiAudience: os.Getenv("OPENFGA_API_AUDIENCE"),
ClientCredentialsApiTokenIssuer: os.Getenv("OPENFGA_API_TOKEN_ISSUER"),
ClientCredentialsClientId: os.Getenv("FGA_CLIENT_ID"),
ClientCredentialsClientSecret: os.Getenv("FGA_CLIENT_SECRET"),
ClientCredentialsApiAudience: os.Getenv("FGA_API_AUDIENCE"),
ClientCredentialsApiTokenIssuer: os.Getenv("FGA_API_TOKEN_ISSUER"),
},
},
})
Expand Down
20 changes: 5 additions & 15 deletions api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,37 +236,27 @@ func (c *APIClient) prepareRequest(
}

// Setup path and query parameters
url, err := url.Parse(path)
uri, err := url.Parse(c.cfg.ApiUrl + path)
if err != nil {
return nil, err
}

// Override request host, if applicable
if c.cfg.ApiHost != "" {
url.Host = c.cfg.ApiHost
}

// Override request scheme, if applicable
if c.cfg.ApiScheme != "" {
url.Scheme = c.cfg.ApiScheme
}

// Adding Query Param
query := url.Query()
query := uri.Query()
for k, v := range queryParams {
for _, iv := range v {
query.Add(k, iv)
}
}

// Encode the parameters.
url.RawQuery = query.Encode()
uri.RawQuery = query.Encode()

// Generate a new request
if body != nil {
localVarRequest, err = http.NewRequest(method, url.String(), body)
localVarRequest, err = http.NewRequest(method, uri.String(), body)
} else {
localVarRequest, err = http.NewRequest(method, url.String(), nil)
localVarRequest, err = http.NewRequest(method, uri.String(), nil)
}
if err != nil {
return nil, err
Expand Down
9 changes: 8 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ var DEFAULT_MAX_METHOD_PARALLEL_REQS = int32(10)

type ClientConfiguration struct {
fgaSdk.Configuration
ApiScheme string `json:"api_scheme,omitempty"`
// ApiScheme - defines the scheme for the API: http or https
// Deprecated: use ApiUrl instead of ApiScheme and ApiHost
ApiScheme string `json:"api_scheme,omitempty"`
// ApiHost - defines the host for the API without the scheme e.g. (api.fga.example)
// Deprecated: use ApiUrl instead of ApiScheme and ApiHost
ApiHost string `json:"api_host,omitempty"`
ApiUrl string `json:"api_url,omitempty"`
StoreId string `json:"store_id,omitempty"`
AuthorizationModelId *string `json:"authorization_model_id,omitempty"`
Credentials *credentials.Credentials `json:"credentials,omitempty"`
Expand All @@ -51,6 +56,7 @@ func newClientConfiguration(cfg *fgaSdk.Configuration) ClientConfiguration {
return ClientConfiguration{
ApiScheme: cfg.ApiScheme,
ApiHost: cfg.ApiHost,
ApiUrl: cfg.ApiUrl,
StoreId: cfg.StoreId,
Credentials: cfg.Credentials,
DefaultHeaders: cfg.DefaultHeaders,
Expand All @@ -70,6 +76,7 @@ func NewSdkClient(cfg *ClientConfiguration) (*OpenFgaClient, error) {
apiConfiguration, err := fgaSdk.NewConfiguration(fgaSdk.Configuration{
ApiScheme: cfg.ApiScheme,
ApiHost: cfg.ApiHost,
ApiUrl: cfg.ApiUrl,
StoreId: cfg.StoreId,
Credentials: cfg.Credentials,
DefaultHeaders: cfg.DefaultHeaders,
Expand Down
103 changes: 89 additions & 14 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
package client_test

import (
"context"
"encoding/json"
"fmt"
"net/http"
"testing"

"github.com/jarcoal/httpmock"
"github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
"context"
"encoding/json"
"fmt"
"net/http"
"testing"

"github.com/jarcoal/httpmock"
"github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
)

type TestDefinition struct {
Expand All @@ -34,7 +34,7 @@ type TestDefinition struct {

func TestOpenFgaClient(t *testing.T) {
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiHost: "api.fga.example",
ApiUrl: "https://api.fga.example",
StoreId: "01GXSB9YR785C4FYS3C0RTG7B2",
})
if err != nil {
Expand All @@ -46,7 +46,39 @@ func TestOpenFgaClient(t *testing.T) {
ApiHost: "api.fga.example",
})
if err != nil {
t.Fatalf("Expect no error when store id is not specified but has %v", err)
t.Fatalf("Expect no error when store id is not specified but got %v", err)
}
})

t.Run("Allow client to have ApiUrl specified", func(t *testing.T) {
_, err := NewSdkClient(&ClientConfiguration{
ApiUrl: "https://api.fga.example",
})
if err != nil {
t.Fatalf("Expect no error when api url is specified but got %v", err)
}
})

t.Run("Ensure that ApiUrl is well formed", func(t *testing.T) {
urls := []string{
"api.fga.example",
"https//api.fga.example",
"https://api.fga.example:notavalidport",
"https://https://api.fga.example",
"notavalidscheme://api.fga.example",
}
for _, uri := range urls {
_, err := NewSdkClient(&ClientConfiguration{
ApiUrl: uri,
})
if err == nil {
t.Fatalf("Expected an error for invalid uri (%s) but got nil", uri)
}

expectedErrorString := fmt.Sprintf("Configuration.ApiUrl (%s) does not form a valid uri", uri)
if err.Error() != expectedErrorString {
t.Fatalf("Expected error (%s) but got (%s)", expectedErrorString, err.Error())
}
}
})

Expand Down Expand Up @@ -92,6 +124,49 @@ func TestOpenFgaClient(t *testing.T) {
}
})

t.Run("Allow specifying an ApiUrl with a port and a base path", func(t *testing.T) {
_, err := NewSdkClient(&ClientConfiguration{
ApiUrl: "https://api.fga.example:8080/fga",
StoreId: "01GXSB9YR785C4FYS3C0RTG7B2",
AuthorizationModelId: openfga.PtrString(""),
})
if err != nil {
t.Fatalf("Expect no error when auth model id is empty but has %v", err)
}

test := TestDefinition{
Name: "ListStores",
JsonResponse: `{"stores":[]}`,
ResponseStatus: http.StatusOK,
Method: http.MethodGet,
}

var expectedResponse openfga.ListStoresResponse
if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil {
t.Fatalf("%v", err)
}

httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores", fgaClient.GetConfig().ApiUrl),
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse)
if err != nil {
return httpmock.NewStringResponse(http.StatusInternalServerError, ""), nil
}
return resp, nil
},
)

got, err := fgaClient.ListStores(context.Background()).Execute()
if err != nil {
t.Fatalf("%v", err)
}
if len(got.Stores) != 0 {
t.Fatalf("expected stores of length 0, got %v", len(got.Stores))
}
})

/* Stores */
t.Run("ListStores", func(t *testing.T) {
test := TestDefinition{
Expand All @@ -108,7 +183,7 @@ func TestOpenFgaClient(t *testing.T) {

httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s://%s/stores", fgaClient.GetConfig().ApiScheme, fgaClient.GetConfig().ApiHost),
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores", fgaClient.GetConfig().ApiUrl),
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse)
if err != nil {
Expand Down Expand Up @@ -159,7 +234,7 @@ func TestOpenFgaClient(t *testing.T) {

httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s://%s/stores", fgaClient.GetConfig().ApiScheme, fgaClient.GetConfig().ApiHost),
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores", fgaClient.GetConfig().ApiUrl),
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse)
if err != nil {
Expand Down Expand Up @@ -246,7 +321,7 @@ func TestOpenFgaClient(t *testing.T) {

httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s://%s/stores", fgaClient.GetConfig().ApiScheme, fgaClient.GetConfig().ApiHost),
httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores", fgaClient.GetConfig().ApiUrl),
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse)
if err != nil {
Expand Down
39 changes: 23 additions & 16 deletions configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
const (
SdkVersion = "0.3.0-beta.1"

defaultUserAgent = "openfga-sdk go/0.3.0-beta.1"
defaultUserAgent = "openfga-sdk go/" + SdkVersion
)

// RetryParams configures configuration for retry in case of HTTP too many request
Expand All @@ -33,8 +33,13 @@ type RetryParams struct {

// Configuration stores the configuration of the API client
type Configuration struct {
ApiScheme string `json:"api_scheme,omitempty"`
// ApiScheme - defines the scheme for the API: http or https
// Deprecated: use ApiUrl instead of ApiScheme and ApiHost
ApiScheme string `json:"api_scheme,omitempty"`
// ApiHost - defines the host for the API without the scheme e.g. (api.fga.example)
// Deprecated: use ApiUrl instead of ApiScheme and ApiHost
ApiHost string `json:"api_host,omitempty"`
ApiUrl string `json:"api_url,omitempty"`
StoreId string `json:"store_id,omitempty"`
Credentials *credentials.Credentials `json:"credentials,omitempty"`
DefaultHeaders map[string]string `json:"default_headers,omitempty"`
Expand All @@ -58,9 +63,19 @@ func GetSdkUserAgent() string {

// NewConfiguration returns a new Configuration object
func NewConfiguration(config Configuration) (*Configuration, error) {
apiUrl := config.ApiUrl

apiScheme := config.ApiScheme
if apiScheme == "" {
apiScheme = "https"
}

if apiUrl == "" {
// If api url is not provided, fall back to deprecated config fields
apiUrl = apiScheme + "://" + config.ApiHost
}
cfg := &Configuration{
ApiScheme: config.ApiScheme,
ApiHost: config.ApiHost,
ApiUrl: apiUrl,
StoreId: config.StoreId,
Credentials: config.Credentials,
DefaultHeaders: config.DefaultHeaders,
Expand All @@ -69,10 +84,6 @@ func NewConfiguration(config Configuration) (*Configuration, error) {
RetryParams: config.RetryParams,
}

if cfg.ApiScheme == "" {
cfg.ApiScheme = "https"
}

if cfg.UserAgent == "" {
cfg.UserAgent = GetSdkUserAgent()
}
Expand All @@ -97,16 +108,12 @@ func (c *Configuration) AddDefaultHeader(key string, value string) {

// ValidateConfig ensures that the given configuration is valid
func (c *Configuration) ValidateConfig() error {
if c.ApiHost == "" {
return reportError("Configuration.ApiHost is required")
}

if c.ApiScheme == "" {
return reportError("Configuration.ApiScheme is required")
if c.ApiUrl == "" {
return reportError("Configuration.ApiUrl is required")
}

if !IsWellFormedUri(c.ApiScheme + "://" + c.ApiHost) {
return reportError("Configuration.ApiScheme and Configuration.ApiHost (%s) do not generate a valid uri", c.ApiScheme+"://"+c.ApiHost)
if !IsWellFormedUri(c.ApiUrl) {
return reportError("Configuration.ApiUrl (%s) does not form a valid uri", c.ApiUrl)
}

if c.Credentials != nil {
Expand Down
4 changes: 3 additions & 1 deletion utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,9 @@ func (v *NullableTime) UnmarshalJSON(src []byte) error {
func IsWellFormedUri(uriString string) bool {
uri, err := url.Parse(uriString)

if (err != nil) || (uri.Scheme != "http" && uri.Scheme != "https") || ((uri.Scheme + "://" + uri.Host) != uriString) {
if err != nil || uri.String() != uriString ||
uri.Host == "" || uri.Host == "http:" || uri.Host == "https:" || // an indicator of a misconfiguration
(uri.Scheme != "http" && uri.Scheme != "https") {
return false
}

Expand Down

0 comments on commit f6bcada

Please sign in to comment.