biscuit-auth / biscuit-go

Apache License 2.0
74 stars 22 forks source link

Should the function generateWorld be accessible ? #147

Open qwackididuck opened 1 month ago

qwackididuck commented 1 month ago

Hi,

We're in charge of taking over a component that generates and consumes biscuits. The component is developed in Java and we want to rewrite it in Go.

I wanted to retrieve a fact from a unmarshalled biscuit.

Example in Java of retrieving the facts (here, the first one)

biscuit.context().stream().filter(Option::isDefined).map(Option::get).findFirst()
.orElseThrow(() -> forbidden());

And the biscuit

user(1233);

In Go, we have no function that allows us to get the facts of a biscuit...

I suppose it could be done by performing Query on the World structure, and the Biscuit structure has a generateWorld method that can create this needed World, but it is private and only used by the tests...

func (b *Biscuit) generateWorld(symbols *datalog.SymbolTable) (*datalog.World, error) {
    world := datalog.NewWorld()

    for _, fact := range *b.authority.facts {
        world.AddFact(fact)
    }

    for _, rule := range b.authority.rules {
        world.AddRule(rule)
    }

    for _, block := range b.blocks {
        for _, fact := range *block.facts {
            world.AddFact(fact)
        }

        for _, rule := range block.rules {
            world.AddRule(rule)
        }
    }

    if err := world.Run(symbols); err != nil {
        return nil, err
    }

    return world, nil
}

Is this a bug ? Shouldn't it be available for consumption ?

If not, how can I retrieve the facts of an unmarshalled biscuit properly ? (no regex and other ugly stuff like that)

Thank you very much

Benoit12345 commented 1 month ago

I think it need to be ! Today, ther's no way to compare 2 biscuits or just read any facts...

qwackididuck commented 1 month ago

Any update on this subject ?

The solution suggested by @Benoit12345 is exactly what I thought.

Do you agree ?

Geal commented 1 month ago

could you tell me more about the use case here? The generateWorld was meant only for tests. if you want to access data in the token, that's supposed to be done through the authorizer API, with methods like Query. A better solution to access the entire list would be the snapshot API that we will introduce soon to this library (already specified and implemented in other libraries): https://github.com/biscuit-auth/biscuit/blob/c87cbb5d778964d6574df3e9e6579567cad12fff/schema.proto#L186-L207 If the goal is to compare tokens there may be other means. What do you want to achieve?

Benoit12345 commented 1 month ago

Thanks @Geal, One of the use case (the main one for us) is for our own tests where we have to check a generated token (ie. same as expected one). For the moment, we use a "very dirty way" by comparing the String() result (which generate sometimes different output and failed our tests with no reason... but that's another subject). We thought to use the authorizer API but using it means that we have, for each generated biscuit, for each use cases, for each facts/rules, to validate every possible values to be sure that the fact/rule is present and validate the right way. That's much more a validation of the authorizer part than a validation of the biscuit itself. Today, for the Go client (it doesn't seem to be the case for other clients), the API is really limited to creation/authorization. It's quite hard to just consult the biscuit content.

qwackididuck commented 1 month ago

Thank you @Geal for your answer

To add more information to @Benoit12345 comment about our use cases :

We have different kinds of biscuit. Each kind allowing access to different kind of resources. Since they are different, they have different authorizers, that are dynamically loaded depending on the type. The type of the biscuit is stored inside of it.

So we can not use the Query API on the authorizer to get the type, since we need the type to get the authorizer :disappointed: In my understanding, the Query API is available on an authorizer, after an Authorize() call.

In my tests :

Query before authorizing

        b, err := biscuit.Unmarshal(token)
    if err != nil {
        panic("unmarhsal failed: " + err.Error())
    }

    fmt.Println(b.String())
    authorizer, err := b.Authorizer(pub)
    if err != nil {
        panic("authorizer creation failed: " + err.Error())
    }

    authorizerRules, err := parser.FromStringRule(`
        my::type($t) <- type($t)
    `)
    if err != nil {
        panic("authorizer contents can not be parsed: " + err.Error())
    }

    authorizer.AddRule(authorizerRules)

    fs, _ := authorizer.Query(authorizerRules)  // fs = [] here

Query after authorizing

        b, err := biscuit.Unmarshal(token)
    if err != nil {
        panic("unmarhsal failed: " + err.Error())
    }

    fmt.Println(b.String())
    authorizer, err := b.Authorizer(pub)
    if err != nil {
        panic("authorizer creation failed: " + err.Error())
    }

    policy, err := parser.FromStringPolicy("allow if type(\"A\")")
    if err != nil {
        panic("unparsable policy: " + err.Error())
    }

    authorizer.AddPolicy(policy)

    if err := authorizer.Authorize(); err != nil {
        panic("failed authorizing: " + err.Error())
    }

    authorizerRules, err := parser.FromStringRule(`
        my::type($t) <- type($t)
    `)
    if err != nil {
        panic("authorizer contents can not be parsed: " + err.Error())
    }

    authorizer.AddRule(authorizerRules)

    fs, _ := authorizer.Query(authorizerRules)  // fs contains the my::type fact

As said in my first message, we have to work with this legacy approach...

With an imaginary example :

  1. We have 2 kinds of biscuit : Type A with the following authorizer

    check if res::user($r), user($u), $u == $r

    Type B with the following authorizer

    check if res::amount($a), max($m), $a <= $m
  2. We generate a biscuit for token A

    type("A")
    user("foo")
  3. The biscuit is used by the client to access a resource, so to get the authorizer, we must retrieve the fact type from the given biscuit.

So how can we do it in your opinion ?

Thank you very much :)

qwackididuck commented 1 month ago

Hello @Geal ,

Do you have any advice on our issue ?

We need to keep the same behaviour, allowed in Java, but it does not seem possible in Golang without the requested evolution..

Or maybe you have a better idea ?

Thank you !

qwackididuck commented 3 weeks ago

Hello,

Does anyone have an opinion ?