(WIP) refine the token constructors:
- for invocation, reorder the parameters for a more "natural language" mental model - for delegation, make "subject" a required parameter to avoid make powerline by mistake - for delegation, implement powerline
This commit is contained in:
@@ -160,13 +160,12 @@ func randToken() (*delegation.Token, cid.Cid, []byte) {
|
|||||||
|
|
||||||
opts := []delegation.Option{
|
opts := []delegation.Option{
|
||||||
delegation.WithExpiration(time.Now().Add(time.Hour)),
|
delegation.WithExpiration(time.Now().Add(time.Hour)),
|
||||||
delegation.WithSubject(iss),
|
|
||||||
}
|
}
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
|
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := delegation.New(iss, aud, cmd, pol, opts...)
|
t, err := delegation.Root(iss, aud, cmd, pol, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,16 +44,15 @@ type Token struct {
|
|||||||
expiration *time.Time
|
expiration *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a validated Token from the provided parameters and options.
|
// New creates a validated delegation Token from the provided parameters and options.
|
||||||
|
// This is typically used to delegate a given power to another agent.
|
||||||
//
|
//
|
||||||
// When creating a delegated token, the Issuer's (iss) DID is assembled
|
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on (subject)".
|
||||||
// using the public key associated with the private key sent as the first
|
func New(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, sub did.DID, opts ...Option) (*Token, error) {
|
||||||
// parameter.
|
|
||||||
func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
|
||||||
tkn := &Token{
|
tkn := &Token{
|
||||||
issuer: iss,
|
issuer: iss,
|
||||||
audience: aud,
|
audience: aud,
|
||||||
subject: did.Undef,
|
subject: sub,
|
||||||
command: cmd,
|
command: cmd,
|
||||||
policy: pol,
|
policy: pol,
|
||||||
meta: meta.NewMeta(),
|
meta: meta.NewMeta(),
|
||||||
@@ -81,16 +80,27 @@ func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Optio
|
|||||||
return tkn, nil
|
return tkn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root creates a validated UCAN delegation Token from the provided
|
// Root creates a validated UCAN delegation Token from the provided parameters and options.
|
||||||
// parameters and options.
|
// This is typically used to create and give a power to an agent.
|
||||||
//
|
//
|
||||||
// When creating a root token, both the Issuer's (iss) and Subject's
|
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on itself".
|
||||||
// (sub) DIDs are assembled from the public key associated with the
|
func Root(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||||
// private key passed as the first argument.
|
return New(iss, aud, cmd, pol, iss, opts...)
|
||||||
func Root(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
}
|
||||||
opts = append(opts, WithSubject(iss))
|
|
||||||
|
|
||||||
return New(iss, aud, cmd, pol, opts...)
|
// Powerline creates a validated UCAN delegation Token from the provided parameters and options.
|
||||||
|
//
|
||||||
|
// Powerline is a pattern for automatically delegating all future delegations to another agent regardless of Subject.
|
||||||
|
// This is a very powerful pattern, use it only if you understand it.
|
||||||
|
// Powerline delegations MUST NOT be used as the root delegation to a resource
|
||||||
|
//
|
||||||
|
// A very common use case for Powerline is providing a stable DID across multiple agents (e.g. representing a user with
|
||||||
|
// multiple devices). This enables the automatic sharing of authority across their devices without needing to share keys
|
||||||
|
// or set up a threshold scheme. It is also flexible, since a Powerline delegation MAY be revoked.
|
||||||
|
//
|
||||||
|
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on anything".
|
||||||
|
func Powerline(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||||
|
return New(iss, aud, cmd, pol, did.Undef, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issuer returns the did.DID representing the Token's issuer.
|
// Issuer returns the did.DID representing the Token's issuer.
|
||||||
|
|||||||
@@ -75,9 +75,8 @@ func TestConstructors(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("New", func(t *testing.T) {
|
t.Run("New", func(t *testing.T) {
|
||||||
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, didtest.PersonaAlice.DID(),
|
||||||
delegation.WithNonce([]byte(nonce)),
|
delegation.WithNonce([]byte(nonce)),
|
||||||
delegation.WithSubject(didtest.PersonaAlice.DID()),
|
|
||||||
delegation.WithExpiration(exp),
|
delegation.WithExpiration(exp),
|
||||||
delegation.WithMeta("foo", "fooo"),
|
delegation.WithMeta("foo", "fooo"),
|
||||||
delegation.WithMeta("bar", "barr"),
|
delegation.WithMeta("bar", "barr"),
|
||||||
@@ -106,6 +105,23 @@ func TestConstructors(t *testing.T) {
|
|||||||
|
|
||||||
golden.Assert(t, string(data), "root.dagjson")
|
golden.Assert(t, string(data), "root.dagjson")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Powerline", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tkn, err := delegation.Powerline(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||||
|
delegation.WithNonce([]byte(nonce)),
|
||||||
|
delegation.WithExpiration(exp),
|
||||||
|
delegation.WithMeta("foo", "fooo"),
|
||||||
|
delegation.WithMeta("bar", "barr"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
golden.Assert(t, string(data), "powerline.dagjson")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncryptedMeta(t *testing.T) {
|
func TestEncryptedMeta(t *testing.T) {
|
||||||
@@ -153,7 +169,7 @@ func TestEncryptedMeta(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||||
delegation.WithEncryptedMetaString(tt.key, tt.value, encryptionKey),
|
delegation.WithEncryptedMetaString(tt.key, tt.value, encryptionKey),
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -191,7 +207,7 @@ func TestEncryptedMeta(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create token with multiple encrypted values
|
// Create token with multiple encrypted values
|
||||||
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...)
|
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ func ExampleNew() {
|
|||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
tkn, err := delegation.New(didtest.PersonaBob.DID(), didtest.PersonaCarol.DID(), cmd, pol,
|
tkn, err := delegation.New(didtest.PersonaBob.DID(), didtest.PersonaCarol.DID(), cmd, pol, didtest.PersonaAlice.DID(),
|
||||||
delegation.WithSubject(didtest.PersonaAlice.DID()),
|
|
||||||
delegation.WithExpirationIn(time.Hour),
|
delegation.WithExpirationIn(time.Hour),
|
||||||
delegation.WithNotBeforeIn(time.Minute),
|
delegation.WithNotBeforeIn(time.Minute),
|
||||||
delegation.WithMeta("foo", "bar"),
|
delegation.WithMeta("foo", "bar"),
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package delegation
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option is a type that allows optional fields to be set during the
|
// Option is a type that allows optional fields to be set during the
|
||||||
@@ -85,20 +83,6 @@ func WithNotBeforeIn(nbf time.Duration) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithSubject sets the Tokens's optional "subject" field to the value of
|
|
||||||
// provided did.DID.
|
|
||||||
//
|
|
||||||
// This Option should only be used with the New constructor - since
|
|
||||||
// Subject is a required parameter when creating a Token via the Root
|
|
||||||
// constructor, any value provided via this Option will be silently
|
|
||||||
// overwritten.
|
|
||||||
func WithSubject(sub did.DID) Option {
|
|
||||||
return func(t *Token) error {
|
|
||||||
t.subject = sub
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithNonce sets the Token's nonce with the given value.
|
// WithNonce sets the Token's nonce with the given value.
|
||||||
// If this option is not used, a random 12-byte nonce is generated for this required field.
|
// If this option is not used, a random 12-byte nonce is generated for this required field.
|
||||||
func WithNonce(nonce []byte) Option {
|
func WithNonce(nonce []byte) Option {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func ExampleNew() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
inv, err := invocation.New(iss, sub, cmd, prf,
|
inv, err := invocation.New(iss, cmd, sub, prf,
|
||||||
invocation.WithArgument("uri", args["uri"]),
|
invocation.WithArgument("uri", args["uri"]),
|
||||||
invocation.WithArgument("headers", args["headers"]),
|
invocation.WithArgument("headers", args["headers"]),
|
||||||
invocation.WithArgument("payload", args["payload"]),
|
invocation.WithArgument("payload", args["payload"]),
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ type Token struct {
|
|||||||
//
|
//
|
||||||
// With the exception of the WithMeta option, all others will overwrite
|
// With the exception of the WithMeta option, all others will overwrite
|
||||||
// the previous contents of their target field.
|
// the previous contents of their target field.
|
||||||
func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) (*Token, error) {
|
//
|
||||||
|
// You can read it as "(Issuer - I) executes (command) on (subject)".
|
||||||
|
func New(iss did.DID, cmd command.Command, sub did.DID, prf []cid.Cid, opts ...Option) (*Token, error) {
|
||||||
iat := time.Now()
|
iat := time.Now()
|
||||||
|
|
||||||
tkn := Token{
|
tkn := Token{
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func TestToken_ExecutionAllowed(t *testing.T) {
|
|||||||
testFails(t, invocation.ErrWrongSub, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidSubject)
|
testFails(t, invocation.ErrWrongSub, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidSubject)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("passes - arguments satify example policy", func(t *testing.T) {
|
t.Run("passes - arguments satisfy example policy", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
testFails(t, invocation.ErrPolicyNotSatisfied, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecInvalidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy)
|
testFails(t, invocation.ErrPolicyNotSatisfied, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecInvalidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy)
|
||||||
@@ -142,7 +142,7 @@ func test(t *testing.T, persona didtest.Persona, cmd command.Command, args *args
|
|||||||
|
|
||||||
opts = append(opts, invocation.WithArguments(args))
|
opts = append(opts, invocation.WithArguments(args))
|
||||||
|
|
||||||
tkn, err := invocation.New(persona.DID(), didtest.PersonaAlice.DID(), cmd, prf, opts...)
|
tkn, err := invocation.New(persona.DID(), cmd, didtest.PersonaAlice.DID(), prf, opts...)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return tkn.ExecutionAllowed(delegationtest.GetDelegationLoader())
|
return tkn.ExecutionAllowed(delegationtest.GetDelegationLoader())
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func TestSealUnsealRoundtrip(t *testing.T) {
|
|||||||
privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew()
|
privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
tkn1, err := invocation.New(iss, sub, cmd, prf,
|
tkn1, err := invocation.New(iss, cmd, sub, prf,
|
||||||
invocation.WithArgument("uri", args["uri"]),
|
invocation.WithArgument("uri", args["uri"]),
|
||||||
invocation.WithArgument("headers", args["headers"]),
|
invocation.WithArgument("headers", args["headers"]),
|
||||||
invocation.WithArgument("payload", args["payload"]),
|
invocation.WithArgument("payload", args["payload"]),
|
||||||
|
|||||||
@@ -38,11 +38,15 @@ func WithArguments(args *args.Args) Option {
|
|||||||
|
|
||||||
// WithAudience sets the Token's audience to the provided did.DID.
|
// WithAudience sets the Token's audience to the provided did.DID.
|
||||||
//
|
//
|
||||||
|
// This can be used if the resource on which the token operates on is different
|
||||||
|
// from the subject. In that situation, the subject is akin to the "service" and
|
||||||
|
// the audience is akin to the resource.
|
||||||
|
//
|
||||||
// If the provided did.DID is the same as the Token's subject, the
|
// If the provided did.DID is the same as the Token's subject, the
|
||||||
// audience is not set.
|
// audience is not set.
|
||||||
func WithAudience(aud did.DID) Option {
|
func WithAudience(aud did.DID) Option {
|
||||||
return func(t *Token) error {
|
return func(t *Token) error {
|
||||||
if t.subject.String() != aud.String() {
|
if t.subject != aud {
|
||||||
t.audience = aud
|
t.audience = aud
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user