Alex Dunmow 6dd745bc34 Let a plugin grant and revoke file access
WO-FL-009. The files family reaches the CMS File access grant store: a plugin
grants one file to a member or an address, lists what a file has handed out,
and revokes what it gave. The response carries the signed unlock link, so a
seller can deliver a file to somebody with no account. Nothing on the wire
names the calling plugin: the host stamps plugin:<name> on what this family
writes and refuses a revoke of anything else. ADR 0008.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 10:47:13 +08:00

80 lines
2.6 KiB
Go

package caps
import (
"context"
"time"
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
"github.com/google/uuid"
"google.golang.org/protobuf/types/known/timestamppb"
)
// filesStub implements plugin.FileGrants over the files.* capability calls.
// The grant's source is the host's to decide, so nothing here names the plugin.
type filesStub struct{ base }
var _ plugin.FileGrants = (*filesStub)(nil)
func (s *filesStub) GrantFileAccess(ctx context.Context, params plugin.FileGrantParams) (plugin.FileGrant, error) {
req := &abiv1.FilesGrantAccessRequest{
AttachmentKind: params.AttachmentKind,
AttachmentId: params.AttachmentID.String(),
Email: params.Email,
}
if params.PublicUserID != uuid.Nil {
req.PublicUserId = params.PublicUserID.String()
}
if !params.ExpiresAt.IsZero() {
req.ExpiresAt = timestamppb.New(params.ExpiresAt)
}
resp := &abiv1.FilesGrantAccessResponse{}
if err := s.invoke(ctx, "grant_access", req, resp); err != nil {
return plugin.FileGrant{}, err
}
return fileGrantFromABI(resp.GetGrant()), nil
}
func (s *filesStub) RevokeFileAccess(ctx context.Context, grantID uuid.UUID) error {
req := &abiv1.FilesRevokeAccessRequest{GrantId: grantID.String()}
return s.invoke(ctx, "revoke_access", req, &abiv1.FilesRevokeAccessResponse{})
}
func (s *filesStub) ListFileGrants(ctx context.Context, attachmentKind string, attachmentID uuid.UUID) ([]plugin.FileGrant, error) {
req := &abiv1.FilesListGrantsRequest{AttachmentKind: attachmentKind, AttachmentId: attachmentID.String()}
resp := &abiv1.FilesListGrantsResponse{}
if err := s.invoke(ctx, "list_grants", req, resp); err != nil {
return nil, err
}
grants := make([]plugin.FileGrant, 0, len(resp.GetGrants()))
for _, grant := range resp.GetGrants() {
grants = append(grants, fileGrantFromABI(grant))
}
return grants, nil
}
func fileGrantFromABI(grant *abiv1.FileAccessGrant) plugin.FileGrant {
if grant == nil {
return plugin.FileGrant{}
}
return plugin.FileGrant{
GrantID: parseUUID(grant.GetGrantId()),
AttachmentKind: grant.GetAttachmentKind(),
AttachmentID: parseUUID(grant.GetAttachmentId()),
PublicUserID: parseUUID(grant.GetPublicUserId()),
Email: grant.GetEmail(),
Source: grant.GetSource(),
ExpiresAt: abiTime(grant.GetExpiresAt()),
RevokedAt: abiTime(grant.GetRevokedAt()),
CreatedAt: abiTime(grant.GetCreatedAt()),
UnlockURL: grant.GetUnlockUrl(),
}
}
func abiTime(stamp *timestamppb.Timestamp) time.Time {
if stamp == nil {
return time.Time{}
}
return stamp.AsTime()
}