Comments (9)
@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.
Thanks for reporting @ghstahl!
There are definitely a few issues here.
-
Object: fgaSdk.PtrString(uuid.New().String())
is not a valid object and we should have returned a 4xx error. We'll tackle this. -
It seems like you had
is anne related to project:X as can_delete?
astrue
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.
StandAlone Test
- Create store, name it
issue152
- 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
- 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
}
- 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
- 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": ""
}
- 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.
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.
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.
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.
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.
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.
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)
- Check gives two responses at random if one of the branches of evaluation errors
- initialize mysql datastore: parse mysql connection dsn: invalid bool value: true HOT 1
- Production MySQL not listing any data HOT 1
- Make ReadAuthorizationModels faster for Postgres and MySQL HOT 4
- Support passing the query Condition Context in Expand
- OpenFGA reports that model is invalid - no entrypoints defined HOT 1
- Error when trying to save the directly pasted authorization models using openfga playground HOT 4
- [Use-Case Question] How to implement negate condition ? HOT 3
- Add support for limiting OIDC clients to specific subjects or other claim data HOT 4
- ListObjects returns error even if there are `max_results` available HOT 1
- If two conditions share the same parameter name but with different types, you get evaluation errors when calling Check or ListObjects HOT 2
- Add CEL cost estimation as part of `WriteAuthorizationModel` HOT 1
- Add dimension on request_duration_by_query_count_ms on whether model is served from cache HOT 7
- Add tests for complex structures in context when a condition parameter is of type any HOT 1
- GoRoutine leak for list objects when there are more than 1000 items to return
- Enable List Objects API pagination HOT 7
- Implement a `singleflight` CheckResolver to avoid concurrency evaluation of overlapping subproblems HOT 1
- Update `openfga/language` Go module for DSL transformer/parser and refactor usages HOT 10
- Get all tuples related to a user without specifying relation or object
- Automated process to update NOTICE file HOT 9
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from openfga.