Code Monkey home page Code Monkey logo

Comments (9)

jon-whit avatar jon-whit commented on May 9, 2024

@ghstahl is there any way you could distill the problem down to a smaller reproducible example with a sample dataset? It's hard to troubleshoot exactly what the issue may be since there are no relationship tuples provided as a sample dataset. If you could get me at least a sample dataset that would be helpful.

from openfga.

rhamzeh avatar rhamzeh commented on May 9, 2024

Thanks for reporting @ghstahl!

There are definitely a few issues here.

  1. Object: fgaSdk.PtrString(uuid.New().String()) is not a valid object and we should have returned a 4xx error. We'll tackle this.

  2. It seems like you had is anne related to project:X as can_delete? as true originally without using the contextual tuples. That is consistent with the API response.
    I'm curious, what was your expectation?

I'll echo what @jon-whit said. If you could provide us with a sample set of tuples (whether here or on https://play.fga.dev), we'd love to be able to help you through with this.

P.S. If you would like to export the tuples you can use:

STORE_ID=... #REPLACE_WITH_YOUR_STORE_ID
curl http://127.0.0.1:8080/stores/$STORE_ID/read-tuples -X POST

For docs on how to get the store id from the playground, you can refer to the docs here

from openfga.

ghstahl avatar ghstahl commented on May 9, 2024

StandAlone Test

  1. Create store, name it issue152
  2. Add the following Model
type organization
  relations
    define base_project_editor as self or project_manager
    define member as self
    define project_editor as base_project_editor and user_in_context
    define project_manager as self and user_in_context
    define user_in_context as self
type project
  relations
    define can_delete as manager
    define can_edit as editor
    define can_view as editor
    define editor as project_editor from owner or project_editor from partner or manager
    define manager as project_manager from owner
    define owner as self
    define partner as self
  1. Add the following tuples.
    This is straight from the following tutorial

there is an error in the documentation. Beth needs to be added to org:B as a project_manager.

I added these by hand in the playground.

body := fgaSdk.WriteRequest{
	Writes:  &fgaSdk.TupleKeys{
		TupleKeys: []fgaSdk.TupleKey { 
		        {
		                // Anne has a `project manager` role at organization A
		                User: fgaSdk.PtrString("anne"),
		                Relation: fgaSdk.PtrString("project_manager"),
		                Object: fgaSdk.PtrString("organization:A"),
	                },
	                {
		                // Anne has a `project manager` role at organization B
		                User: fgaSdk.PtrString("anne"),
		                Relation: fgaSdk.PtrString("project_manager"),
		                Object: fgaSdk.PtrString("organization:B"),
	                },
	                {
		                // Anne has a `project manager` role at organization C
		                User: fgaSdk.PtrString("anne"),
		                Relation: fgaSdk.PtrString("project_manager"),
		                Object: fgaSdk.PtrString("organization:C"),
	                },
	                {
		                // Beth has a `project manager` role at organization B
		                User: fgaSdk.PtrString("beth"),
		                Relation: fgaSdk.PtrString("project_manager"),
		                Object: fgaSdk.PtrString("organization:B"),
	                },
	                {
		                // Carl has a `project manager` role at organization C
		                User: fgaSdk.PtrString("carl"),
		                Relation: fgaSdk.PtrString("project_manager"),
		                Object: fgaSdk.PtrString("organization:C"),
	                },
	                {
		                // Organization A owns Project X
		                User: fgaSdk.PtrString("organization:A"),
		                Relation: fgaSdk.PtrString("owner"),
		                Object: fgaSdk.PtrString("project:X"),
	                },
	                {
		                // Project X is shared with Organization B
		                User: fgaSdk.PtrString("organization:B"),
		                Relation: fgaSdk.PtrString("partner"),
		                Object: fgaSdk.PtrString("project:X"),
	                }, 
		},
	},
}

_, response, err := fgaClient.OpenFgaApi.Write(context.Background()).Body(body).Execute()
if err != nil {
    // .. Handle error
}
  1. playground queries
is anne related to organization:A as project_manager?
   false.    This is because anne isnt a user_in_context to organization:A
is anne related to project:X as can_delete?
   false.    This is because anne isnt a user_in_context to organization:A

Lets add anne:
USER:         anne
RELATION: user_in_context
OBJECT:     organization:A

is anne related to organization:A as project_manager?
   true
is anne related to project:X as can_delete?
   true
  1. Tuples
{
    "tuples": [
        {
            "key": {
                "object": "organization:A",
                "relation": "project_manager",
                "user": "anne"
            },
            "timestamp": "2022-08-08T16:17:57.454Z"
        },
        {
            "key": {
                "object": "organization:B",
                "relation": "project_manager",
                "user": "anne"
            },
            "timestamp": "2022-08-08T16:18:11.968135Z"
        },
        {
            "key": {
                "object": "organization:C",
                "relation": "project_manager",
                "user": "anne"
            },
            "timestamp": "2022-08-08T16:18:29.876606Z"
        },
        {
            "key": {
                "object": "organization:B",
                "relation": "project_manager",
                "user": "beth"
            },
            "timestamp": "2022-08-08T16:19:22.469647Z"
        },
        {
            "key": {
                "object": "organization:C",
                "relation": "project_manager",
                "user": "carl"
            },
            "timestamp": "2022-08-08T16:19:35.648110Z"
        },
        {
            "key": {
                "object": "project:X",
                "relation": "owner",
                "user": "organization:A"
            },
            "timestamp": "2022-08-08T16:19:49.237763Z"
        },
        {
            "key": {
                "object": "project:X",
                "relation": "partner",
                "user": "organization:B"
            },
            "timestamp": "2022-08-08T16:20:02.614081Z"
        },
        {
            "key": {
                "object": "organization:A",
                "relation": "user_in_context",
                "user": "anne"
            },
            "timestamp": "2022-08-08T16:39:47.504544Z"
        }
    ],
    "continuation_token": ""
}
  1. Stand-Alone Source Code
package main

import (
	"context"
	"errors"
	"fmt"
	"os"

	"github.com/google/uuid"
	fgaSdk "github.com/openfga/go-sdk"
	"github.com/rs/zerolog/log"
)

func main() {
	DoContextAuthorizaiton()
}

func DumpStores() {
	configuration, err := fgaSdk.NewConfiguration(fgaSdk.Configuration{
		ApiScheme: os.Getenv("OPENFGA_API_SCHEME"), // optional, defaults to "https"
		ApiHost:   os.Getenv("OPENFGA_API_HOST"),   // required, define without the scheme (e.g. api.openfga.example instead of https://api.openfga.example)
	})

	if err != nil {
		// .. Handle error
		log.Fatal().Err(err).Msg("Error creating configuration")
	}
	apiClient := fgaSdk.NewAPIClient(configuration)
	continuationToken := ""
	pageSize := int32(10)
	for {
		data, httpResponse, err := apiClient.OpenFgaApi.
			ListStores(context.Background()).
			ContinuationToken(continuationToken).
			PageSize(pageSize).
			Execute()
		if err != nil {
			// .. Handle error
			log.Fatal().Err(err).Msg("Error creating configuration")
		}
		if httpResponse.StatusCode != 200 {
			// .. Handle error
			log.Fatal().Int("statusCode", httpResponse.StatusCode).Msg("Failed to get stores")
		}
		if data.Stores == nil || len(*data.Stores) == 0 {
			break
		}
		for _, store := range *data.Stores {
			log.Info().Interface("store", store).Send()
		}
		if data.ContinuationToken == nil || len(*data.ContinuationToken) == 0 {
			break
		}
		continuationToken = *data.ContinuationToken
	}
}

func GetStoreIDByName(storeName string) (string, error) {
	configuration, err := fgaSdk.NewConfiguration(fgaSdk.Configuration{
		ApiScheme: os.Getenv("OPENFGA_API_SCHEME"), // optional, defaults to "https"
		ApiHost:   os.Getenv("OPENFGA_API_HOST"),   // required, define without the scheme (e.g. api.openfga.example instead of https://api.openfga.example)
	})

	if err != nil {
		// .. Handle error
		log.Fatal().Err(err).Msg("Error creating configuration")
	}
	apiClient := fgaSdk.NewAPIClient(configuration)
	continuationToken := ""
	pageSize := int32(1)
	storeID := ""
	for {
		data, httpResponse, err := apiClient.OpenFgaApi.
			ListStores(context.Background()).
			ContinuationToken(continuationToken).
			PageSize(pageSize).
			Execute()
		if err != nil {
			return "", err
		}
		if httpResponse.StatusCode != 200 {
			return "", err
		}
		if data.Stores == nil || len(*data.Stores) == 0 {
			break
		}

		for _, store := range *data.Stores {
			if store.Name != nil {
				if *store.Name == storeName {
					return *store.Id, nil
				}
			}
		}
		if len(storeID) > 0 {
			// found store in the previous for loop
			break
		}
		if data.ContinuationToken == nil || len(*data.ContinuationToken) == 0 {
			break
		}
		continuationToken = *data.ContinuationToken
	}
	return "", errors.New("store not found")
}

func GetApiClientByStoreID(storeID string) (*fgaSdk.APIClient, error) {
	configuration, err := fgaSdk.NewConfiguration(fgaSdk.Configuration{
		ApiScheme: os.Getenv("OPENFGA_API_SCHEME"), // optional, defaults to "https"
		ApiHost:   os.Getenv("OPENFGA_API_HOST"),   // required, define without the scheme (e.g. api.openfga.example instead of https://api.openfga.example)
	})
	log.Info().Msgf("Found store: %s", storeID)
	configuration.StoreId = storeID

	if err != nil {
		// .. Handle error
		log.Fatal().Err(err).Msg("Error creating configuration")
	}
	apiClient := fgaSdk.NewAPIClient(configuration)
	return apiClient, nil
}

func DoContextAuthorizaiton() {
	DumpStores()
	storeName := "issue152"
	storeID, err := GetStoreIDByName(storeName)
	if err != nil {
		log.Fatal().Err(err).Msg("Fail to get store ID")
	}
	fgaClient, err := GetApiClientByStoreID(storeID)
	if err != nil {
		log.Fatal().Err(err).Msg("Fail to get API client")
	}
	// is anne related to project:X as can_delete?
	//  returns true in playground
	//  no context here, so bad, because anne doesn't have can_delete to project:X in other orgs
	body := fgaSdk.CheckRequest{
		TupleKey: &fgaSdk.TupleKey{
			User:     fgaSdk.PtrString("anne"),
			Relation: fgaSdk.PtrString("can_delete"),
			Object:   fgaSdk.PtrString("project:X"),
		},
	}
	data, response, err := fgaClient.OpenFgaApi.Check(context.Background()).Body(body).Execute()
	if err != nil {
		log.Error().Err(err).Msg("Failed to check permission")
	} else {
		log.Info().Int("StatusCode", response.StatusCode).Send()
		log.Info().Interface("data", data).Send()
	}

	// is anne related to project:X as can_delete?  IN CONTEXT NOW
	//  Lets do a garbage request to see if it works
	body = fgaSdk.CheckRequest{
		TupleKey: &fgaSdk.TupleKey{
			User:     fgaSdk.PtrString("anne"),
			Relation: fgaSdk.PtrString("can_delete"),
			Object:   fgaSdk.PtrString("project:X"),
		},
		ContextualTuples: &fgaSdk.ContextualTupleKeys{
			TupleKeys: []fgaSdk.TupleKey{
				{
					User:     fgaSdk.PtrString("anne"),
					Relation: fgaSdk.PtrString("user_in_context"),
					Object:   fgaSdk.PtrString(fmt.Sprintf("organization:%s", uuid.New().String())),
				},
			},
		},
	}
	data, response, err = fgaClient.OpenFgaApi.Check(context.Background()).Body(body).Execute()
	if err != nil {
		log.Error().Err(err).Msg("Failed to check permission")
	} else {
		log.Info().Int("StatusCode", response.StatusCode).Send()
		log.Info().Interface("data", data).Send()
	}

}

from openfga.

ghstahl avatar ghstahl commented on May 9, 2024

It seems like you had is anne related to project:X as can_delete? as true originally without using the contextual tuples. That is consistent with the API response.
I'm curious, what was your expectation?
Yes I expect this to be the case, because it is accurate under this context.

I fixed the invalid object with the same result.

{
	User:     fgaSdk.PtrString("anne"),
	Relation: fgaSdk.PtrString("user_in_context"),
	Object:   fgaSdk.PtrString(fmt.Sprintf("organization:%s", uuid.New().String())),
}

from openfga.

ghstahl avatar ghstahl commented on May 9, 2024

I can also reproduce this just using postman

Request

curl --location --request POST 'http://localhost:3601/stores/01G9Z3NC2VFD7BGSXFSZ42ESN8/check' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data-raw '{
  "tuple_key": {
    "object": "project:X",
    "relation": "can_delete",
    "user": "anne"
  },
  "contextual_tuples": {
    "tuple_keys": [
      {
        "object": "in:flames",
        "relation": "user_in_context",
        "user": "anne"
      } 
    ]
  } 
}'

Response

{
    "allowed": true,
    "resolution": ""
}

I would think that I should get a 200 back from this, because I haven't gotten around to make the association from anne to organization:DOESNOTEXITYET as user_in_context. or the organization:EXISTS but we don't have a relationship.

Anyway, in both cases it doesn't exist from anne, thus I expect a false back.

from openfga.

rhamzeh avatar rhamzeh commented on May 9, 2024

Looking at the tuples you posted, it seems that the last tuple may be the issue you have:

        {
            "key": {
                "object": "organization:A",
                "relation": "user_in_context",
                "user": "anne"
            },
            "timestamp": "2022-08-08T16:39:47.504544Z"
        }

This means that OpenFGA will always treat anne as user_in_context of organization:A

Which may be why you are getting true instead of false.

When you are passing the following as a contextual tuple, it is in addition to whatever other tuples already exist and will not replace them:

      {
        "object": "in:flames",
        "relation": "user_in_context",
        "user": "anne"
      } 

So you may want to delete that last tuple above and retry.

from openfga.

ghstahl avatar ghstahl commented on May 9, 2024

given this tutorial organization-context-authorization it has a final Model that added user_in_context to the organization.

type organization
  relations
    define member as self
    define project_manager as self and user_in_context
    define base_project_editor as self or project_manager
    define project_editor as base_project_editor and user_in_context
    define user_in_context as self

It instructs me to make a relationship between anne and organization:A as user_in_context.

body := fgaSdk.WriteRequest{
	Writes:  &fgaSdk.TupleKeys{
		TupleKeys: []fgaSdk.TupleKey { 
			{
				// Anne is authorizing from the context of organization:A
				User: fgaSdk.PtrString("anne"),
				Relation: fgaSdk.PtrString("user_in_context"),
				Object: fgaSdk.PtrString("organization:A"),
			}, 
		},
	},
}

_, response, err := fgaClient.OpenFgaApi.Write(context.Background()).Body(body).Execute()
if err != nil {
    // .. Handle error
}

then follows up by asking if anne is in the org

body := fgaSdk.CheckRequest{
	TupleKey: &fgaSdk.TupleKey{
		User: fgaSdk.PtrString("anne"),
		Relation: fgaSdk.PtrString("can_view"),
		Object: fgaSdk.PtrString("project:X"),
	},
	ContextualTuples: &fgaSdk.ContextualTuples{
		TupleKeys: []fgaSdk.TupleKey{
			{
				User: fgaSdk.PtrString("anne"),
				Relation: fgaSdk.PtrString("user_in_context"),
				Object: fgaSdk.PtrString("organization:A"),
			}
		}
	}
}
data, response, err := fgaClient.OpenFgaApi.Check(context.Background()).Body(body).Execute()

// data = { allowed: true }

This all makes sense to me on the surface, so I don't know what to do now as the tutorial says to do it.

from openfga.

rhamzeh avatar rhamzeh commented on May 9, 2024

In the final step Using Contextual Tuples, it is describing the problems of what was done in the previous step (writing the tuple), but it does not expressly indicate that the previously written tuple from the previous step should be deleted.

Sorry about that and the confusion it caused. We'll fix it within the next two days and get back to you.

Thanks for reporting that @ghstahl!

from openfga.

ghstahl avatar ghstahl commented on May 9, 2024

Ok got it to work by deleting is anne related to organization:A as user_in_context?

When I had that relationship, it completely made sense to me that it should work, but no! More importantly I felt I could easily explain the concept of context.

I am imagining having to explain this to someone new in a year and this is how it goes.

Me > So you see, you have to add anne is a project_manager of organization:A for user_in_context to come into play.
Other Engineer > But there is a define user_in_context as self and it looks exactly like that member one.
Me > Yea yea, I know there is a define user_in_context as self but don't set it EVER!
Other Engineer > I don't get it!
Me > its magic, it gets created on the fly only when you use contextual tuples, and then it's gone.
Other Engineer > Really?

The thing with this is that this is such an important concept that it has to be a first-class thing. It will eventually be in everyone's model, especially for enterprise offerings.

For me it was requirement # 1.

All joking aside, it does have an elegant aspect to it, but damn its confusing. I totally get it now.

from openfga.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.