diff --git a/commands/principal/cbor_gen.go b/commands/principal/cbor_gen.go new file mode 100644 index 0000000..bf65ab2 --- /dev/null +++ b/commands/principal/cbor_gen.go @@ -0,0 +1,148 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package principal + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *InvalidateArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Tenant (did.DID) (struct) + if len("tenant") > 8192 { + return xerrors.Errorf("Value in field \"tenant\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("tenant"))); err != nil { + return err + } + if _, err := cw.WriteString(string("tenant")); err != nil { + return err + } + + if err := t.Tenant.MarshalCBOR(cw); err != nil { + return err + } + + // t.Principal (string) (string) + if len("principal") > 8192 { + return xerrors.Errorf("Value in field \"principal\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("principal"))); err != nil { + return err + } + if _, err := cw.WriteString(string("principal")); err != nil { + return err + } + + if len(t.Principal) > 8192 { + return xerrors.Errorf("Value in field t.Principal was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Principal))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Principal)); err != nil { + return err + } + return nil +} + +func (t *InvalidateArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = InvalidateArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("InvalidateArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Tenant (did.DID) (struct) + case "tenant": + + { + + if err := t.Tenant.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Tenant: %w", err) + } + + } + // t.Principal (string) (string) + case "principal": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Principal = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/commands/principal/codec_test.go b/commands/principal/codec_test.go new file mode 100644 index 0000000..95f1fd7 --- /dev/null +++ b/commands/principal/codec_test.go @@ -0,0 +1,79 @@ +//go:build !codegen + +package principal_test + +import ( + "bytes" + "reflect" + "testing" + + "github.com/fil-forge/libforge/commands/principal" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/testutil" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/stretchr/testify/require" +) + +var tenant = did.MustParse("did:plc:ewvi7nxzyoun6zhxrhs64oiz") + +func TestInvalidateArgumentsRoundTrip(t *testing.T) { + in := &principal.InvalidateArguments{ + Tenant: tenant, + Principal: "8f2c9e14", + } + + var cb bytes.Buffer + require.NoError(t, in.MarshalCBOR(&cb)) + var outCBOR principal.InvalidateArguments + require.NoError(t, outCBOR.UnmarshalCBOR(bytes.NewReader(cb.Bytes()))) + require.True(t, reflect.DeepEqual(*in, outCBOR), "CBOR round-trip mismatch:\n got %#v\nwant %#v", outCBOR, *in) + + var jb bytes.Buffer + require.NoError(t, in.MarshalDagJSON(&jb)) + require.Equal(t, `{"principal":"8f2c9e14","tenant":"`+tenant.String()+`"}`, jb.String()) + var outJSON principal.InvalidateArguments + require.NoError(t, outJSON.UnmarshalDagJSON(bytes.NewReader(jb.Bytes())), "json: %s", jb.String()) + require.True(t, reflect.DeepEqual(*in, outJSON), "DAG-JSON round-trip mismatch:\n got %#v\nwant %#v", outJSON, *in) +} + +// The command is self-signed: Hilt is both issuer and subject, because no +// delegation names a principal and Swarf authorizes the invocation from its +// publisher list instead of a proof chain. +func TestInvalidateInvokeUnpack(t *testing.T) { + hilt := testutil.RandomIssuer(t) + swarf := testutil.RandomIssuer(t) + + args := &principal.InvalidateArguments{Tenant: tenant, Principal: "8f2c9e14"} + inv, err := principal.Invalidate.Invoke( + hilt, + hilt.DID(), + args, + invocation.WithAudience(swarf.DID()), + invocation.WithNoNonce(), + invocation.WithNoExpiration(), + ) + require.NoError(t, err) + require.Equal(t, "/principal/invalidate", inv.Command().String()) + require.Equal(t, hilt.DID(), inv.Issuer()) + require.Equal(t, hilt.DID(), inv.Subject()) + require.Equal(t, swarf.DID(), inv.Audience()) + require.Empty(t, inv.Proofs()) + + var seen *principal.InvalidateArguments + handler := principal.Invalidate.Handler(func(req *binding.Request[*principal.InvalidateArguments], res *binding.Response[*principal.InvalidateOK]) error { + seen = req.Task().Arguments() + return res.SetSuccess(&principal.InvalidateOK{}) + }) + + res, err := execution.NewResponse(inv.Task().Link(), execution.WithIssuer(swarf)) + require.NoError(t, err) + require.NoError(t, handler(execution.NewRequest(t.Context(), inv), res)) + + require.Equal(t, args, seen) + + out, err := principal.Invalidate.Unpack(res.Receipt()) + require.NoError(t, err) + require.Equal(t, &principal.InvalidateOK{}, out) +} diff --git a/commands/principal/gen/main.go b/commands/principal/gen/main.go new file mode 100644 index 0000000..87a98a5 --- /dev/null +++ b/commands/principal/gen/main.go @@ -0,0 +1,41 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/libforge/commands/principal" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + principal.InvalidateArguments{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "principal", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "principal", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/commands/principal/invalidate.go b/commands/principal/invalidate.go new file mode 100644 index 0000000..9cc5f2d --- /dev/null +++ b/commands/principal/invalidate.go @@ -0,0 +1,19 @@ +//go:build !codegen + +package principal + +import ( + "github.com/fil-forge/libforge/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type InvalidateOK = commands.Unit + +// Invalidate is the `/principal/invalidate` command. Hilt invokes it on Swarf +// as its service identity before it commits a change to what a principal can +// reach, recording that every proof a gateway cached for that principal's +// access keys is void. No delegation names a principal, so there is no proof +// chain to carry the authority: Swarf accepts the command only from issuers in +// its publisher list. +var Invalidate = binding.Bind[*InvalidateArguments, *InvalidateOK](command.MustParse("/principal/invalidate")) diff --git a/commands/principal/json_gen.go b/commands/principal/json_gen.go new file mode 100644 index 0000000..92c7978 --- /dev/null +++ b/commands/principal/json_gen.go @@ -0,0 +1,150 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package principal + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *InvalidateArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Principal (string) (string) + if len("principal") > 8192 { + return fmt.Errorf("string in field \"principal\" was too long") + } + if err := jw.WriteString(string("principal")); err != nil { + return fmt.Errorf("writing string for field \"principal\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Principal) > 8192 { + return fmt.Errorf("string in field t.Principal was too long") + } + if err := jw.WriteString(string(t.Principal)); err != nil { + return fmt.Errorf("writing string for field t.Principal: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Tenant (did.DID) (struct) + if len("tenant") > 8192 { + return fmt.Errorf("string in field \"tenant\" was too long") + } + if err := jw.WriteString(string("tenant")); err != nil { + return fmt.Errorf("writing string for field \"tenant\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Tenant.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Tenant: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *InvalidateArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = InvalidateArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for InvalidateArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for InvalidateArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for InvalidateArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field InvalidateArguments: string too large") + } + return fmt.Errorf("reading string for field InvalidateArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field InvalidateArguments: %w", err) + } + switch name { + + // t.Principal (string) (string) + case "principal": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.Principal: string too long") + } + return fmt.Errorf("reading string for field t.Principal: %w", err) + } + t.Principal = string(sval) + } + + // t.Tenant (did.DID) (struct) + case "tenant": + + if err := t.Tenant.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Tenant: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for InvalidateArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field InvalidateArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for InvalidateArguments") + } + } + } + + return nil +} diff --git a/commands/principal/types.go b/commands/principal/types.go new file mode 100644 index 0000000..64dd658 --- /dev/null +++ b/commands/principal/types.go @@ -0,0 +1,12 @@ +package principal + +import "github.com/fil-forge/ucantone/did" + +// InvalidateArguments are the arguments to `/principal/invalidate`. +type InvalidateArguments struct { + // Tenant is the DID of the tenant the principal belongs to. + Tenant did.DID `cborgen:"tenant" dagjsongen:"tenant"` + // Principal is the identifier of the principal whose cached proofs are + // void, unique within the tenant. + Principal string `cborgen:"principal" dagjsongen:"principal"` +}