Skip to content

Commit

Permalink
[FGA-98] Add ConvertSchemaToResourceTypes and ConvertResourceTypesToS…
Browse files Browse the repository at this point in the history
…chema to FGA module (#364)

* Add ConvertSchemaToResourceTypes and ConvertResourceTypesToSchema to FGA module

* Add live tests

* CR feedback

* Properly adapt error if a struct that contains message field

* CR feedback

* CR review
  • Loading branch information
atainter authored Aug 28, 2024
1 parent 55d7825 commit 24b104a
Show file tree
Hide file tree
Showing 7 changed files with 425 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
*.swp
*.log
*.vscode

.idea/
105 changes: 105 additions & 0 deletions pkg/fga/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"

Expand All @@ -20,6 +21,10 @@ import (
// ResponseLimit is the default number of records to limit a response to.
const ResponseLimit = 10

const (
SchemaConvertEndpoint = "%s/fga/v1/schemas/convert"
)

// Order represents the order of records.
type Order string

Expand Down Expand Up @@ -407,6 +412,39 @@ type QueryResponse struct {
ListMetadata common.ListMetadata `json:"list_metadata"`
}

// Schema
type ConvertSchemaToResourceTypesOpts struct {
// The schema to convert to resource types.
Schema string
}

type ConvertSchemaWarning struct {
// The warning message.
Message string `json:"message"`
}

type ConvertResourceTypesToSchemaOpts struct {
// The version of the transpiler to use.
Version string `json:"version"`

// The resource types to convert to a schema.
ResourceTypes []ResourceType `json:"resource_types"`
}

type ConvertSchemaResponse struct {
// The version transpiler used to convert the schema.
Version string `json:"version"`

// Warnings generated from schema issues.
Warnings []ConvertSchemaWarning `json:"warnings,omitempty"`

// The schema generated from the resource types.
Schema *string `json:"schema,omitempty"`

// The resource types generated from the schema.
ResourceTypes []ResourceType `json:"resource_types,omitempty"`
}

// GetResource gets a Resource.
func (c *Client) GetResource(ctx context.Context, opts GetResourceOpts) (Resource, error) {
c.once.Do(c.init)
Expand Down Expand Up @@ -925,3 +963,70 @@ func (c *Client) Query(ctx context.Context, opts QueryOpts) (QueryResponse, erro
err = dec.Decode(&body)
return body, err
}

// ConvertSchemaToResourceTypes converts a schema to resource types.
func (c *Client) ConvertSchemaToResourceTypes(ctx context.Context, opts ConvertSchemaToResourceTypesOpts) (ConvertSchemaResponse, error) {
c.once.Do(c.init)

endpoint := fmt.Sprintf(SchemaConvertEndpoint, c.Endpoint)
req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(opts.Schema))
if err != nil {
return ConvertSchemaResponse{}, err
}

req = req.WithContext(ctx)
req.Header.Set("Content-Type", "text/plain")
req.Header.Set("Authorization", "Bearer "+c.APIKey)
req.Header.Set("User-Agent", "workos-go/"+workos.Version)

res, err := c.HTTPClient.Do(req)
if err != nil {
return ConvertSchemaResponse{}, err
}
defer res.Body.Close()

if err = workos_errors.TryGetHTTPError(res); err != nil {
return ConvertSchemaResponse{}, err
}

var body ConvertSchemaResponse
dec := json.NewDecoder(res.Body)
err = dec.Decode(&body)
return body, err
}

// ConvertResourceTypesToSchema converts resource types to a schema.
func (c *Client) ConvertResourceTypesToSchema(ctx context.Context, opts ConvertResourceTypesToSchemaOpts) (ConvertSchemaResponse, error) {
c.once.Do(c.init)

data, err := c.JSONEncode(opts)
if err != nil {
return ConvertSchemaResponse{}, err
}

endpoint := fmt.Sprintf(SchemaConvertEndpoint, c.Endpoint)
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(data))
if err != nil {
return ConvertSchemaResponse{}, err
}

req = req.WithContext(ctx)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.APIKey)
req.Header.Set("User-Agent", "workos-go/"+workos.Version)

res, err := c.HTTPClient.Do(req)
if err != nil {
return ConvertSchemaResponse{}, err
}
defer res.Body.Close()

if err = workos_errors.TryGetHTTPError(res); err != nil {
return ConvertSchemaResponse{}, err
}

var body ConvertSchemaResponse
dec := json.NewDecoder(res.Body)
err = dec.Decode(&body)
return body, err
}
84 changes: 84 additions & 0 deletions pkg/fga/client_live_example.go
Original file line number Diff line number Diff line change
Expand Up @@ -1892,3 +1892,87 @@ func TestQueryWarrants(t *testing.T) {
t.Fatal(err)
}
}

func TestConvertSchemaLive(t *testing.T) {
setup()
schema := "version 0.1\n\ntype report\n relation owner []\n relation editor []\n relation viewer []\n \n inherit editor if\n relation owner\n \n inherit viewer if\n relation editor"

response, err := ConvertSchemaToResourceTypes(context.Background(), ConvertSchemaToResourceTypesOpts{
Schema: schema,
})
if err != nil {
t.Fatal(err)
}

require.Len(t, response.ResourceTypes, 1)
require.Equal(t, response.Version, "0.1")
require.Equal(t, response, ConvertSchemaResponse{
Version: "0.1",
ResourceTypes: []ResourceType{
{
Type: "report",
Relations: map[string]interface{}{
"owner": map[string]interface{}{},
"editor": map[string]interface{}{
"inherit_if": "owner",
},
"viewer": map[string]interface{}{
"inherit_if": "editor",
},
},
},
},
})
}

func TestConvertResourceTypesLive(t *testing.T) {
setup()

response, err := ConvertResourceTypesToSchema(context.Background(), ConvertResourceTypesToSchemaOpts{
Version: "0.1",
ResourceTypes: []ResourceType{
{
Type: "report",
Relations: map[string]interface{}{
"owner": map[string]interface{}{},
"editor": map[string]interface{}{
"inherit_if": "owner",
},
"viewer": map[string]interface{}{
"inherit_if": "editor",
},
},
},
},
})
if err != nil {
t.Fatal(err)
}

require.Equal(t, response.Version, "0.1")
require.NotNil(t, response.Schema)

resourceTypeResponse, err := ConvertSchemaToResourceTypes(context.Background(), ConvertSchemaToResourceTypesOpts{
Schema: *response.Schema,
})
if err != nil {
t.Fatal(err)
}
require.Equal(t, resourceTypeResponse, ConvertSchemaResponse{
Version: "0.1",
ResourceTypes: []ResourceType{
{
Type: "report",
Relations: map[string]interface{}{
"owner": map[string]interface{}{},
"editor": map[string]interface{}{
"inherit_if": "owner",
},
"viewer": map[string]interface{}{
"inherit_if": "editor",
},
},
},
},
})
}
116 changes: 116 additions & 0 deletions pkg/fga/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1316,3 +1316,119 @@ func queryTestHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(body)
}

func convertSchemaToResourceTypesTestHandler(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth != "Bearer test" {
http.Error(w, "bad auth", http.StatusUnauthorized)
return
}

body, err := json.Marshal(ConvertSchemaResponse{
Version: "0.1",
ResourceTypes: []ResourceType{
{
Type: "report",
Relations: map[string]interface{}{
"owner": map[string]interface{}{},
"editor": map[string]interface{}{
"inherit_if": "owner",
},
"viewer": map[string]interface{}{
"inherit_if": "editor",
},
},
},
},
})

if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
w.Write(body)
}

func TestConvertSchemaToResourceTypes(t *testing.T) {
tests := []struct {
scenario string
client *Client
options ConvertSchemaToResourceTypesOpts
expected ConvertSchemaResponse
err bool
}{
{
scenario: "Request without API Key returns an error",
client: &Client{},
err: true,
},
{
scenario: "Request returns ResourceTypes",
client: &Client{
APIKey: "test",
},
options: ConvertSchemaToResourceTypesOpts{
Schema: "version 0.1\n\ntype report\n relation owner []\n relation editor []\n relation viewer []\n \n inherit editor if\n relation owner\n \n inherit viewer if\n relation editor",
},
expected: ConvertSchemaResponse{
Version: "0.1",
ResourceTypes: []ResourceType{
{
Type: "report",
Relations: map[string]interface{}{
"owner": map[string]interface{}{},
"editor": map[string]interface{}{
"inherit_if": "owner",
},
"viewer": map[string]interface{}{
"inherit_if": "editor",
},
},
},
},
},
},
}

for _, test := range tests {
t.Run(test.scenario, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(convertSchemaToResourceTypesTestHandler))
defer server.Close()

client := test.client
client.Endpoint = server.URL
client.HTTPClient = &retryablehttp.HttpClient{Client: *server.Client()}

resourceTypes, err := client.ConvertSchemaToResourceTypes(context.Background(), test.options)
if test.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, test.expected, resourceTypes)
})
}
}

func convertResourceTypesToSchemaTestHandler(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth != "Bearer test" {
http.Error(w, "bad auth", http.StatusUnauthorized)
return
}

schema := "version 0.1\n\ntype report\n relation owner []\n relation editor []\n relation viewer []\n \n inherit editor if\n relation owner\n \n inherit viewer if\n relation editor"
body, err := json.Marshal(ConvertSchemaResponse{
Version: "0.1",
Schema: &schema,
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
w.Write(body)
}
14 changes: 14 additions & 0 deletions pkg/fga/fga.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,17 @@ func Query(
) (QueryResponse, error) {
return DefaultClient.Query(ctx, opts)
}

func ConvertSchemaToResourceTypes(
ctx context.Context,
opts ConvertSchemaToResourceTypesOpts,
) (ConvertSchemaResponse, error) {
return DefaultClient.ConvertSchemaToResourceTypes(ctx, opts)
}

func ConvertResourceTypesToSchema(
ctx context.Context,
opts ConvertResourceTypesToSchemaOpts,
) (ConvertSchemaResponse, error) {
return DefaultClient.ConvertResourceTypesToSchema(ctx, opts)
}
Loading

0 comments on commit 24b104a

Please sign in to comment.