feat(delegation): add validation/accessors
This commit is contained in:
@@ -4,7 +4,6 @@ package delegation
|
||||
|
||||
import (
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -25,13 +24,6 @@ func applyConfigOptions(c *config, options ...Option) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithExpiration(o *time.Time) Option {
|
||||
return func(c *config) error {
|
||||
c.Expiration = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMeta(o map[string]datamodel.Node) Option {
|
||||
return func(c *config) error {
|
||||
c.Meta = o
|
||||
@@ -39,31 +31,9 @@ func WithMeta(o map[string]datamodel.Node) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithNoExpiration(o bool) Option {
|
||||
return func(c *config) error {
|
||||
c.NoExpiration = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithNotBefore(o *time.Time) Option {
|
||||
return func(c *config) error {
|
||||
c.NotBefore = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSubject is a did.DID representing the Subject.
|
||||
func WithSubject(o *did.DID) Option {
|
||||
return func(c *config) error {
|
||||
c.Subject = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithPowerline(o bool) Option {
|
||||
return func(c *config) error {
|
||||
c.Powerline = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package delegation
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -23,30 +24,22 @@ type Delegation struct {
|
||||
}
|
||||
|
||||
//go:generate -command options go run github.com/selesy/go-options
|
||||
//go:generate options -type=config -prefix=With -output=delegatiom_options.go -cmp=false -stringer=false -imports=time,github.com/ucan-wg/go-ucan/did,github.com/ipld/go-ipld-prime/datamodel
|
||||
//go:generate options -type=config -prefix=With -output=delegatiom_options.go -cmp=false -stringer=false -imports=time,github.com/ipld/go-ipld-prime/datamodel
|
||||
|
||||
type config struct {
|
||||
Expiration *time.Time
|
||||
Meta map[string]datamodel.Node
|
||||
NoExpiration bool
|
||||
NotBefore *time.Time
|
||||
// is a did.DID representing the Subject.
|
||||
Subject *did.DID
|
||||
Powerline bool
|
||||
Meta map[string]datamodel.Node
|
||||
NotBefore *time.Time
|
||||
}
|
||||
|
||||
// Required fields for delegation
|
||||
|
||||
// Requirements for root
|
||||
|
||||
func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command, pol *policy.Policy, exp *time.Time, nonce []byte, opts ...Option) (*Delegation, error) {
|
||||
func New(privKey crypto.PrivKey, aud did.DID, sub *did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, exp *time.Time, opts ...Option) (*Delegation, error) {
|
||||
cfg, err := newConfig(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !iss.Defined() {
|
||||
return nil, fmt.Errorf("%w: %s", token.ErrMissingRequiredDID, "iss")
|
||||
issuer, err := did.FromPrivKey(privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !aud.Defined() {
|
||||
@@ -55,8 +48,8 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command,
|
||||
audience := aud.String()
|
||||
|
||||
var subject *string
|
||||
if cfg.Subject != nil && cfg.Subject.Defined() {
|
||||
s := cfg.Subject.String()
|
||||
if sub != nil {
|
||||
s := sub.String()
|
||||
subject = &s
|
||||
}
|
||||
|
||||
@@ -65,7 +58,11 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce = []uint8(nonce)
|
||||
var meta *token.Map__String__Any
|
||||
if len(cfg.Meta) > 0 {
|
||||
m := token.ToIPLDMapStringAny(cfg.Meta)
|
||||
meta = &m
|
||||
}
|
||||
|
||||
var notBefore *int
|
||||
if cfg.NotBefore != nil {
|
||||
@@ -73,20 +70,14 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command,
|
||||
notBefore = &n
|
||||
}
|
||||
|
||||
var meta *token.Map__String__Any
|
||||
if len(cfg.Meta) > 0 {
|
||||
m := token.ToIPLDMapStringAny(cfg.Meta)
|
||||
meta = &m
|
||||
}
|
||||
|
||||
var expiration *int
|
||||
if exp != nil && !cfg.NoExpiration {
|
||||
e := int(cfg.NotBefore.Unix())
|
||||
if exp != nil {
|
||||
e := int(exp.Unix())
|
||||
expiration = &e
|
||||
}
|
||||
|
||||
tkn := &token.Token{
|
||||
Issuer: iss.String(),
|
||||
Issuer: issuer.String(),
|
||||
Audience: &audience,
|
||||
Subject: subject,
|
||||
Command: cmd.String(),
|
||||
@@ -111,16 +102,82 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command,
|
||||
return dlg, nil
|
||||
}
|
||||
|
||||
type validateFunc func() error
|
||||
func Root(privKey crypto.PrivKey, aud did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, exp *time.Time, opts ...Option) (*Delegation, error) {
|
||||
sub, err := did.FromPrivKey(privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return New(privKey, aud, &sub, cmd, pol, nonce, exp, opts...)
|
||||
}
|
||||
|
||||
func (d *Delegation) Audience() did.DID {
|
||||
id, _ := did.Parse(*d.envel.TokenPayload().Audience)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (d *Delegation) Command() *command.Command {
|
||||
cmd, _ := command.Parse(d.envel.TokenPayload().Command)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (d *Delegation) IsPowerline() bool {
|
||||
return d.envel.TokenPayload().Subject == nil
|
||||
}
|
||||
|
||||
func (d *Delegation) IsRoot() bool {
|
||||
return &d.envel.TokenPayload().Issuer == d.envel.TokenPayload().Subject
|
||||
}
|
||||
|
||||
func (d *Delegation) Issuer() did.DID {
|
||||
id, _ := did.Parse(d.envel.TokenPayload().Issuer)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (d *Delegation) Meta() map[string]datamodel.Node {
|
||||
return d.envel.TokenPayload().Meta.Values
|
||||
}
|
||||
|
||||
func (d *Delegation) Nonce() []byte {
|
||||
return *d.envel.TokenPayload().Nonce
|
||||
}
|
||||
|
||||
func (d *Delegation) Policy() policy.Policy {
|
||||
pol, _ := policy.FromIPLD(*d.envel.TokenPayload().Policy)
|
||||
|
||||
return pol
|
||||
}
|
||||
|
||||
func (d *Delegation) Subject() *did.DID {
|
||||
if d.envel.TokenPayload().Subject == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
id, _ := did.Parse(*d.envel.TokenPayload().Subject)
|
||||
|
||||
return &id
|
||||
}
|
||||
|
||||
func (d *Delegation) Validate() error {
|
||||
return errors.Join(
|
||||
d.validateDID("iss", &d.envel.TokenPayload().Issuer, false),
|
||||
d.validateDID("aud", d.envel.TokenPayload().Audience, false),
|
||||
d.validateDID("sub", d.envel.TokenPayload().Subject, true),
|
||||
d.validateCommand(),
|
||||
d.validatePolicy(),
|
||||
d.validateNonce(),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *Delegation) validateCommand() error {
|
||||
_, err := command.Parse(d.envel.TokenPayload().Command)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Delegation) validateDID(fieldName string, identity *string, nullableOrOptional bool) error {
|
||||
if identity == nil && !nullableOrOptional {
|
||||
return fmt.Errorf("a required DID is missing: %s", fieldName)
|
||||
@@ -137,3 +194,31 @@ func (d *Delegation) validateDID(fieldName string, identity *string, nullableOrO
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Delegation) validateNonce() error {
|
||||
if d.envel.TokenPayload().Nonce == nil || len(*d.envel.TokenPayload().Nonce) < 1 {
|
||||
return fmt.Errorf("nonce is required: must not be nil or empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Delegation) validatePolicy() error {
|
||||
if d.envel.TokenPayload().Policy == nil {
|
||||
return fmt.Errorf("the \"pol\" field is required")
|
||||
}
|
||||
|
||||
_, err := policy.FromIPLD(*d.envel.TokenPayload().Policy)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func Nonce() ([]byte, error) {
|
||||
nonce := make([]byte, 32)
|
||||
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
149
delegation/delegation_test.go
Normal file
149
delegation/delegation_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package delegation_test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/capability/command"
|
||||
"github.com/ucan-wg/go-ucan/capability/policy"
|
||||
"github.com/ucan-wg/go-ucan/delegation"
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
const (
|
||||
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
||||
|
||||
AudiencePrivKeyCfg = "CAESQL1hvbXpiuk2pWr/XFbfHJcZNpJ7S90iTA3wSCTc/BPRneCwPnCZb6c0vlD6ytDWqaOt0HEOPYnqEpnzoBDprSM="
|
||||
AudienceDID = "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv"
|
||||
|
||||
issuerPrivKeyCfg = "CAESQLSql38oDmQXIihFFaYIjb73mwbPsc7MIqn4o8PN4kRNnKfHkw5gRP1IV9b6d0estqkZayGZ2vqMAbhRixjgkDU="
|
||||
issuerDID = "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"
|
||||
|
||||
subjectPrivKeyCfg = "CAESQL9RtjZ4dQBeXtvDe53UyvslSd64kSGevjdNiA1IP+hey5i/3PfRXSuDr71UeJUo1fLzZ7mGldZCOZL3gsIQz5c="
|
||||
subjectDID = "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
||||
subJectCmd = "/foo/bar"
|
||||
subjectPol = `
|
||||
[
|
||||
[
|
||||
"==",
|
||||
".status",
|
||||
"draft"
|
||||
],
|
||||
[
|
||||
"all",
|
||||
".reviewer",
|
||||
[
|
||||
"like",
|
||||
".email",
|
||||
"*@example.com"
|
||||
]
|
||||
],
|
||||
[
|
||||
"any",
|
||||
".tags",
|
||||
[
|
||||
"or",
|
||||
[
|
||||
[
|
||||
"==",
|
||||
".",
|
||||
"news"
|
||||
],
|
||||
[
|
||||
"==",
|
||||
".",
|
||||
"press"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
`
|
||||
)
|
||||
|
||||
func TestConstructors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
privKey := privKey(t, issuerPrivKeyCfg)
|
||||
|
||||
aud, err := did.Parse(AudienceDID)
|
||||
|
||||
sub, err := did.Parse(subjectDID)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd, err := command.Parse(subJectCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
pol, err := policy.FromDagJson(subjectPol)
|
||||
require.NoError(t, err)
|
||||
|
||||
exp := time.Time{}
|
||||
|
||||
meta := map[string]datamodel.Node{
|
||||
"foo": basicnode.NewString("fooo"),
|
||||
"bar": basicnode.NewString("barr"),
|
||||
}
|
||||
|
||||
t.Run("New", func(t *testing.T) {
|
||||
dlg, err := delegation.New(privKey, aud, &sub, cmd, pol, []byte(nonce), &exp, delegation.WithMeta(meta))
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := dlg.ToDagJson()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log(string(data))
|
||||
|
||||
golden.Assert(t, string(data), "new.dagjson")
|
||||
})
|
||||
|
||||
t.Run("Root", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dlg, err := delegation.Root(privKey, aud, cmd, pol, []byte(nonce), &exp, delegation.WithMeta(meta))
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := dlg.ToDagJson()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log(string(data))
|
||||
|
||||
golden.Assert(t, string(data), "root.dagjson")
|
||||
})
|
||||
}
|
||||
|
||||
func privKey(t *testing.T, privKeyCfg string) crypto.PrivKey {
|
||||
t.Helper()
|
||||
|
||||
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
||||
require.NoError(t, err)
|
||||
|
||||
return privKey
|
||||
}
|
||||
|
||||
func TestKey(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
priv, _, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
privMar, err := crypto.MarshalPrivateKey(priv)
|
||||
require.NoError(t, err)
|
||||
|
||||
privCfg := crypto.ConfigEncodeKey(privMar)
|
||||
t.Log(privCfg)
|
||||
|
||||
id, err := did.FromPubKey(priv.GetPublic())
|
||||
require.NoError(t, err)
|
||||
t.Log(id)
|
||||
|
||||
t.Fail()
|
||||
}
|
||||
1
delegation/testdata/new.dagjson
vendored
Normal file
1
delegation/testdata/new.dagjson
vendored
Normal file
@@ -0,0 +1 @@
|
||||
[{"/":{"bytes":"P2lPLfdMuZuc4NPZ0mbozU+/bn5xoWlJsu+Fvaxi4ICYXVJb9/wiTTht3WJEFqjxXLxfTl4BMZF3J1CNvMPqBg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":-62135596800,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"}}]
|
||||
1
delegation/testdata/root.dagjson
vendored
Normal file
1
delegation/testdata/root.dagjson
vendored
Normal file
@@ -0,0 +1 @@
|
||||
[{"/":{"bytes":"0sjiwG9BOgpezz6qw5UiD+rqOeqFLn4+Qds1PvbnsUBoc3RhF6IVxIeoOXDh1ufv3RHaI/zg4wjYpUwAMpTACw"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":-62135596800,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"}}]
|
||||
Reference in New Issue
Block a user