Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ gen:
rm ./errors/datamodel/*_gen.go || true
cd ./errors/datamodel/gen && go run ./main.go

rm ./examples/types/cbor_gen.go || true
rm ./examples/types/cbor_gen.go ./examples/types/fields/*_gen.go || true
cd ./examples/types/gen && go run ./main.go

rm ./result/datamodel/*_gen.go || true
Expand All @@ -35,6 +35,9 @@ gen:

cd ./ucan/delegation/policy/datamodel/gen && go run ./main.go

rm ./ucan/delegation/policy/policytest/fields/*_gen.go || true
cd ./ucan/delegation/policy/policytest/gen && go run ./main.go

rm ./ucan/delegation/policy/internal/fixtures/datamodel/*_gen.go || true
cd ./ucan/delegation/policy/internal/fixtures/datamodel/gen && go run ./main.go

Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,27 @@ invocation, err := messageSend.Invoke(
)
```

#### Typed policies

Policies can be authored against generated field descriptors instead of raw jq
selector strings. A descriptor is generated from a command's argument struct
(see the `fieldgen` package), so the comparison value is type-checked against
the field and the selector path is derived from a real field — a wrong type or
a mistyped path is a compile error. The builders return the same statements as
the string-selector builders, so they drop straight into `policy.Build`.

See examples in [policies_test.go](./examples/policies_test.go)

```go
pol, err := policy.Build(
// every recipient must be an example.com address
policy.Each(fields.MessageSendArguments.To, func(addr policy.Selector[string]) []policy.StatementBuilderFunc {
return []policy.StatementBuilderFunc{policy.Glob(addr, "*@example.com")}
}),
policy.Eq(fields.MessageSendArguments.Subject, "Hello!"),
)
```

#### Container

See examples in [container_test.go](./examples/container_test.go)
Expand Down
28 changes: 28 additions & 0 deletions examples/policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package examples
import (
"testing"

"github.com/fil-forge/ucantone/examples/types/fields"
"github.com/fil-forge/ucantone/ipld"
"github.com/fil-forge/ucantone/ucan/delegation/policy"
)
Expand Down Expand Up @@ -46,3 +47,30 @@ func TestParsePolicy(t *testing.T) {
panic("policy did not match")
}
}

// TestTypedPolicy authors the same shape of policy against generated field
// descriptors (see examples/types/fields). The selector paths and value types
// come from the MessageSendArguments struct, so a wrong-typed value or a
// mistyped path would be a compile error rather than a runtime mismatch.
func TestTypedPolicy(t *testing.T) {
msg := ipld.Map{
"to": []string{"bob@example.com", "carol@example.com"},
"subject": "Hello!",
"message": "Hi there",
}

pol, err := policy.Build(
// every recipient must be an example.com address
policy.Each(fields.MessageSendArguments.To, func(addr policy.Selector[string]) []policy.StatementBuilderFunc {
return []policy.StatementBuilderFunc{policy.Glob(addr, "*@example.com")}
}),
policy.Eq(fields.MessageSendArguments.Subject, "Hello!"),
)
if err != nil {
panic(err)
}

if err := policy.Match(pol, msg); err != nil {
panic("policy did not match")
}
}
122 changes: 122 additions & 0 deletions examples/typed_policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package examples

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/fil-forge/ucantone/examples/types"
"github.com/fil-forge/ucantone/examples/types/fields"
"github.com/fil-forge/ucantone/principal/ed25519"
"github.com/fil-forge/ucantone/ucan"
"github.com/fil-forge/ucantone/ucan/container"
"github.com/fil-forge/ucantone/ucan/delegation"
"github.com/fil-forge/ucantone/ucan/delegation/policy"
"github.com/fil-forge/ucantone/ucan/invocation"
"github.com/fil-forge/ucantone/validator"
)

// TestTypedPolicyEndToEnd walks the full lifecycle of a *typed* delegation
// policy: a service delegates a command to a user under a policy, the user
// invokes the command, and the validator enforces the policy against the
// invocation's arguments.
//
// The policy is authored against generated field descriptors
// (examples/types/fields, produced from types.MessageSendArguments by the
// fieldgen generator). Each builder takes a typed Selector, so:
//
// - the comparison value is checked against the field's Go type, and
// - the selector path (".to", ".subject", ...) is derived from a real field.
//
// A wrong-typed value or a mistyped path is therefore a compile error, not a
// policy that silently never matches at runtime. For example, these would not
// compile:
//
// policy.Eq(fields.MessageSendArguments.Subject, 42) // .subject is a string field
// policy.Glob(fields.MessageSendArguments.To, "*") // .to is a SliceSelector, not Selector[string]
// policy.Gt(fields.MessageSendArguments.To, nil) // a list field is not Ordered
//
// The builders return the same policy.StatementBuilderFunc as the legacy
// string-selector builders, so they drop straight into
// delegation.WithPolicyBuilder and reuse the matcher and wire format unchanged.
func TestTypedPolicyEndToEnd(t *testing.T) {
// mailer owns the /message/send capability; alice is the user it delegates to.
mailer, err := ed25519.Generate()
require.NoError(t, err)
alice, err := ed25519.Generate()
require.NoError(t, err)

const messageSend = "/message/send"

// mailer delegates /message/send to alice, but constrains how she may use
// it with a typed policy:
// - every recipient must be an example.com address,
// - the subject must not be empty, and
// - the message body must mention "ucantone".
dlg, err := delegation.Delegate(
mailer, // issuer (root authority over the capability)
alice.DID(), // audience (who receives the delegation)
mailer.DID(), // subject (the resource the capability acts on)
messageSend, // command

Check failure on line 60 in examples/typed_policy_test.go

View workflow job for this annotation

GitHub Actions / Run tests

cannot use messageSend (untyped string constant "/message/send") as ucan.Command value in argument to delegation.Delegate
delegation.WithPolicyBuilder(
// fields.MessageSendArguments.To is a SliceSelector[Selector[string]];
// Each hands the closure the element descriptor — here an identity
// Selector[string] pointing at each address in turn.
policy.Each(fields.MessageSendArguments.To, func(addr policy.Selector[string]) []policy.StatementBuilderFunc {
return []policy.StatementBuilderFunc{policy.Glob(addr, "*@example.com")}
}),
// .subject is a Selector[string]; Ne pins the value type to string.
policy.Ne(fields.MessageSendArguments.Subject, ""),
policy.Glob(fields.MessageSendArguments.Message, "*ucantone*"),
),
)
require.NoError(t, err)

// invoke builds a /message/send invocation carrying typed arguments and the
// delegation above as proof of authority.
invoke := func(args *types.MessageSendArguments) ucan.Invocation {
inv, err := invocation.Invoke(alice, mailer.DID(), messageSend, args, invocation.WithProofs(dlg.Link()))

Check failure on line 78 in examples/typed_policy_test.go

View workflow job for this annotation

GitHub Actions / Run tests

cannot use messageSend (untyped string constant "/message/send") as ucan.Command value in argument to invocation.Invoke
require.NoError(t, err)
return inv
}

// validate runs the full pipeline: signature + time bounds + proof chain +
// the policy check (cap.Allows -> policy.Match) against the decoded args.
// The delegation is supplied to the validator via a container so its proof
// resolver can walk the chain.
validate := func(inv ucan.Invocation) error {
return validator.ValidateInvocation(t.Context(), inv,
validator.WithProofResolver(validator.ProofsFromContainer(
container.New(container.WithDelegations(dlg)),
)),
)
}

// 1) A fully conforming invocation passes validation.
require.NoError(t, validate(invoke(&types.MessageSendArguments{
To: []string{"bob@example.com", "carol@example.com"},
Subject: "Status update",
Message: "the ucantone migration is done",
})), "all clauses satisfied")

// 2) A recipient outside example.com is rejected by the Each/Glob clause.
require.Error(t, validate(invoke(&types.MessageSendArguments{
To: []string{"bob@example.com", "mallory@evil.test"},
Subject: "Status update",
Message: "the ucantone migration is done",
})), "a non-example.com recipient violates the policy")

// 3) An empty subject is rejected by the Ne clause.
require.Error(t, validate(invoke(&types.MessageSendArguments{
To: []string{"bob@example.com"},
Subject: "",
Message: "the ucantone migration is done",
})), "an empty subject violates the policy")

// 4) A message that doesn't mention "ucantone" is rejected by the Glob clause.
require.Error(t, validate(invoke(&types.MessageSendArguments{
To: []string{"bob@example.com"},
Subject: "Status update",
Message: "unrelated chatter",
})), "a message missing the keyword violates the policy")
}
41 changes: 41 additions & 0 deletions examples/types/fields/policy_fields_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions examples/types/gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"github.com/fil-forge/ucantone/examples/types"
"github.com/fil-forge/ucantone/ucan/delegation/policy/fieldgen"
cbg "github.com/whyrusleeping/cbor-gen"
)

Expand All @@ -14,4 +15,17 @@ func main() {
); err != nil {
panic(err)
}

// Typed policy field descriptors for the same argument types, written to a
// sibling package so the entry-point vars (e.g. fields.MessageSend) and the
// descriptor types (e.g. fields.MessageSendArgumentsFields) don't collide
// with the source types. PromisedMsgSendArguments is omitted: its
// "await/ok" map key is not a valid jq selector segment.
if err := fieldgen.WriteFieldDescriptors("../fields/policy_fields_gen.go", "fields",
types.EmailsListArguments{},
types.MessageSendArguments{},
types.EchoArguments{},
); err != nil {
panic(err)
}
}
Loading
Loading