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>
This commit is contained in:
parent
5f0cc20bbf
commit
6dd745bc34
@ -974,3 +974,53 @@ message HttpRequestResponse {
|
|||||||
// redirects.
|
// redirects.
|
||||||
string final_url = 4;
|
string final_url = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- files.* (plugin.FileGrants, WO-FL-009) ---
|
||||||
|
//
|
||||||
|
// A File access grant hands one person one file whatever tier they hold (ADR
|
||||||
|
// 0181, cms repo). Every grant written here is attributed to the calling
|
||||||
|
// plugin, and a plugin may revoke only the grants it wrote.
|
||||||
|
|
||||||
|
// FileAccessGrant mirrors plugin.FileGrant. Exactly one subject is set.
|
||||||
|
message FileAccessGrant {
|
||||||
|
string grant_id = 1; // UUID
|
||||||
|
string attachment_kind = 2; // "library" or "table"
|
||||||
|
string attachment_id = 3; // UUID
|
||||||
|
string public_user_id = 4; // UUID, empty when the subject is an address
|
||||||
|
string email = 5; // empty when the subject is a member
|
||||||
|
string source = 6; // "plugin:<name>" for anything this family wrote
|
||||||
|
google.protobuf.Timestamp expires_at = 7;
|
||||||
|
google.protobuf.Timestamp revoked_at = 8;
|
||||||
|
google.protobuf.Timestamp created_at = 9;
|
||||||
|
// Signed link that opens this file for this grant, absolute on the site.
|
||||||
|
// Empty when the file has no permanent link to sign.
|
||||||
|
string unlock_url = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FilesGrantAccessRequest {
|
||||||
|
string attachment_kind = 1; // "library" (default) or "table"
|
||||||
|
string attachment_id = 2; // UUID
|
||||||
|
string public_user_id = 3; // UUID; set this or email, never both
|
||||||
|
string email = 4;
|
||||||
|
// Optional. Absent means the grant ends only when it is revoked.
|
||||||
|
google.protobuf.Timestamp expires_at = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FilesGrantAccessResponse {
|
||||||
|
FileAccessGrant grant = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FilesRevokeAccessRequest {
|
||||||
|
string grant_id = 1; // UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
message FilesRevokeAccessResponse {}
|
||||||
|
|
||||||
|
message FilesListGrantsRequest {
|
||||||
|
string attachment_kind = 1; // "library" (default) or "table"
|
||||||
|
string attachment_id = 2; // UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
message FilesListGrantsResponse {
|
||||||
|
repeated FileAccessGrant grants = 1;
|
||||||
|
}
|
||||||
|
|||||||
@ -8399,6 +8399,422 @@ func (x *HttpRequestResponse) GetFinalUrl() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileAccessGrant mirrors plugin.FileGrant. Exactly one subject is set.
|
||||||
|
type FileAccessGrant struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
GrantId string `protobuf:"bytes,1,opt,name=grant_id,json=grantId,proto3" json:"grant_id,omitempty"` // UUID
|
||||||
|
AttachmentKind string `protobuf:"bytes,2,opt,name=attachment_kind,json=attachmentKind,proto3" json:"attachment_kind,omitempty"` // "library" or "table"
|
||||||
|
AttachmentId string `protobuf:"bytes,3,opt,name=attachment_id,json=attachmentId,proto3" json:"attachment_id,omitempty"` // UUID
|
||||||
|
PublicUserId string `protobuf:"bytes,4,opt,name=public_user_id,json=publicUserId,proto3" json:"public_user_id,omitempty"` // UUID, empty when the subject is an address
|
||||||
|
Email string `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"` // empty when the subject is a member
|
||||||
|
Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` // "plugin:<name>" for anything this family wrote
|
||||||
|
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
|
||||||
|
RevokedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=revoked_at,json=revokedAt,proto3" json:"revoked_at,omitempty"`
|
||||||
|
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||||
|
// Signed link that opens this file for this grant, absolute on the site.
|
||||||
|
// Empty when the file has no permanent link to sign.
|
||||||
|
UnlockUrl string `protobuf:"bytes,10,opt,name=unlock_url,json=unlockUrl,proto3" json:"unlock_url,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) Reset() {
|
||||||
|
*x = FileAccessGrant{}
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[157]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*FileAccessGrant) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[157]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use FileAccessGrant.ProtoReflect.Descriptor instead.
|
||||||
|
func (*FileAccessGrant) Descriptor() ([]byte, []int) {
|
||||||
|
return file_v1_capability_proto_rawDescGZIP(), []int{157}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) GetGrantId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.GrantId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) GetAttachmentKind() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AttachmentKind
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) GetAttachmentId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AttachmentId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) GetPublicUserId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicUserId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) GetEmail() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Email
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) GetSource() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Source
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) GetExpiresAt() *timestamppb.Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.ExpiresAt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) GetRevokedAt() *timestamppb.Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.RevokedAt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) GetCreatedAt() *timestamppb.Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.CreatedAt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FileAccessGrant) GetUnlockUrl() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UnlockUrl
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilesGrantAccessRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
AttachmentKind string `protobuf:"bytes,1,opt,name=attachment_kind,json=attachmentKind,proto3" json:"attachment_kind,omitempty"` // "library" (default) or "table"
|
||||||
|
AttachmentId string `protobuf:"bytes,2,opt,name=attachment_id,json=attachmentId,proto3" json:"attachment_id,omitempty"` // UUID
|
||||||
|
PublicUserId string `protobuf:"bytes,3,opt,name=public_user_id,json=publicUserId,proto3" json:"public_user_id,omitempty"` // UUID; set this or email, never both
|
||||||
|
Email string `protobuf:"bytes,4,opt,name=email,proto3" json:"email,omitempty"`
|
||||||
|
// Optional. Absent means the grant ends only when it is revoked.
|
||||||
|
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessRequest) Reset() {
|
||||||
|
*x = FilesGrantAccessRequest{}
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[158]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*FilesGrantAccessRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[158]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use FilesGrantAccessRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*FilesGrantAccessRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_v1_capability_proto_rawDescGZIP(), []int{158}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessRequest) GetAttachmentKind() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AttachmentKind
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessRequest) GetAttachmentId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AttachmentId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessRequest) GetPublicUserId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicUserId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessRequest) GetEmail() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Email
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessRequest) GetExpiresAt() *timestamppb.Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.ExpiresAt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilesGrantAccessResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Grant *FileAccessGrant `protobuf:"bytes,1,opt,name=grant,proto3" json:"grant,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessResponse) Reset() {
|
||||||
|
*x = FilesGrantAccessResponse{}
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[159]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*FilesGrantAccessResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[159]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use FilesGrantAccessResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*FilesGrantAccessResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_v1_capability_proto_rawDescGZIP(), []int{159}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesGrantAccessResponse) GetGrant() *FileAccessGrant {
|
||||||
|
if x != nil {
|
||||||
|
return x.Grant
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilesRevokeAccessRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
GrantId string `protobuf:"bytes,1,opt,name=grant_id,json=grantId,proto3" json:"grant_id,omitempty"` // UUID
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesRevokeAccessRequest) Reset() {
|
||||||
|
*x = FilesRevokeAccessRequest{}
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[160]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesRevokeAccessRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*FilesRevokeAccessRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *FilesRevokeAccessRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[160]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use FilesRevokeAccessRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*FilesRevokeAccessRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_v1_capability_proto_rawDescGZIP(), []int{160}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesRevokeAccessRequest) GetGrantId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.GrantId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilesRevokeAccessResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesRevokeAccessResponse) Reset() {
|
||||||
|
*x = FilesRevokeAccessResponse{}
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[161]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesRevokeAccessResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*FilesRevokeAccessResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *FilesRevokeAccessResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[161]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use FilesRevokeAccessResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*FilesRevokeAccessResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_v1_capability_proto_rawDescGZIP(), []int{161}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilesListGrantsRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
AttachmentKind string `protobuf:"bytes,1,opt,name=attachment_kind,json=attachmentKind,proto3" json:"attachment_kind,omitempty"` // "library" (default) or "table"
|
||||||
|
AttachmentId string `protobuf:"bytes,2,opt,name=attachment_id,json=attachmentId,proto3" json:"attachment_id,omitempty"` // UUID
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesListGrantsRequest) Reset() {
|
||||||
|
*x = FilesListGrantsRequest{}
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[162]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesListGrantsRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*FilesListGrantsRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *FilesListGrantsRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[162]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use FilesListGrantsRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*FilesListGrantsRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_v1_capability_proto_rawDescGZIP(), []int{162}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesListGrantsRequest) GetAttachmentKind() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AttachmentKind
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesListGrantsRequest) GetAttachmentId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AttachmentId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilesListGrantsResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Grants []*FileAccessGrant `protobuf:"bytes,1,rep,name=grants,proto3" json:"grants,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesListGrantsResponse) Reset() {
|
||||||
|
*x = FilesListGrantsResponse{}
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[163]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesListGrantsResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*FilesListGrantsResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *FilesListGrantsResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_v1_capability_proto_msgTypes[163]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use FilesListGrantsResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*FilesListGrantsResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_v1_capability_proto_rawDescGZIP(), []int{163}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FilesListGrantsResponse) GetGrants() []*FileAccessGrant {
|
||||||
|
if x != nil {
|
||||||
|
return x.Grants
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var File_v1_capability_proto protoreflect.FileDescriptor
|
var File_v1_capability_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_v1_capability_proto_rawDesc = "" +
|
const file_v1_capability_proto_rawDesc = "" +
|
||||||
@ -8965,7 +9381,40 @@ const file_v1_capability_proto_rawDesc = "" +
|
|||||||
"\tfinal_url\x18\x04 \x01(\tR\bfinalUrl\x1aP\n" +
|
"\tfinal_url\x18\x04 \x01(\tR\bfinalUrl\x1aP\n" +
|
||||||
"\fHeadersEntry\x12\x10\n" +
|
"\fHeadersEntry\x12\x10\n" +
|
||||||
"\x03key\x18\x01 \x01(\tR\x03key\x12*\n" +
|
"\x03key\x18\x01 \x01(\tR\x03key\x12*\n" +
|
||||||
"\x05value\x18\x02 \x01(\v2\x14.abi.v1.HeaderValuesR\x05value:\x028\x01B5Z3git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1b\x06proto3"
|
"\x05value\x18\x02 \x01(\v2\x14.abi.v1.HeaderValuesR\x05value:\x028\x01\"\x9e\x03\n" +
|
||||||
|
"\x0fFileAccessGrant\x12\x19\n" +
|
||||||
|
"\bgrant_id\x18\x01 \x01(\tR\agrantId\x12'\n" +
|
||||||
|
"\x0fattachment_kind\x18\x02 \x01(\tR\x0eattachmentKind\x12#\n" +
|
||||||
|
"\rattachment_id\x18\x03 \x01(\tR\fattachmentId\x12$\n" +
|
||||||
|
"\x0epublic_user_id\x18\x04 \x01(\tR\fpublicUserId\x12\x14\n" +
|
||||||
|
"\x05email\x18\x05 \x01(\tR\x05email\x12\x16\n" +
|
||||||
|
"\x06source\x18\x06 \x01(\tR\x06source\x129\n" +
|
||||||
|
"\n" +
|
||||||
|
"expires_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x129\n" +
|
||||||
|
"\n" +
|
||||||
|
"revoked_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\trevokedAt\x129\n" +
|
||||||
|
"\n" +
|
||||||
|
"created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"unlock_url\x18\n" +
|
||||||
|
" \x01(\tR\tunlockUrl\"\xde\x01\n" +
|
||||||
|
"\x17FilesGrantAccessRequest\x12'\n" +
|
||||||
|
"\x0fattachment_kind\x18\x01 \x01(\tR\x0eattachmentKind\x12#\n" +
|
||||||
|
"\rattachment_id\x18\x02 \x01(\tR\fattachmentId\x12$\n" +
|
||||||
|
"\x0epublic_user_id\x18\x03 \x01(\tR\fpublicUserId\x12\x14\n" +
|
||||||
|
"\x05email\x18\x04 \x01(\tR\x05email\x129\n" +
|
||||||
|
"\n" +
|
||||||
|
"expires_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"I\n" +
|
||||||
|
"\x18FilesGrantAccessResponse\x12-\n" +
|
||||||
|
"\x05grant\x18\x01 \x01(\v2\x17.abi.v1.FileAccessGrantR\x05grant\"5\n" +
|
||||||
|
"\x18FilesRevokeAccessRequest\x12\x19\n" +
|
||||||
|
"\bgrant_id\x18\x01 \x01(\tR\agrantId\"\x1b\n" +
|
||||||
|
"\x19FilesRevokeAccessResponse\"f\n" +
|
||||||
|
"\x16FilesListGrantsRequest\x12'\n" +
|
||||||
|
"\x0fattachment_kind\x18\x01 \x01(\tR\x0eattachmentKind\x12#\n" +
|
||||||
|
"\rattachment_id\x18\x02 \x01(\tR\fattachmentId\"J\n" +
|
||||||
|
"\x17FilesListGrantsResponse\x12/\n" +
|
||||||
|
"\x06grants\x18\x01 \x03(\v2\x17.abi.v1.FileAccessGrantR\x06grantsB5Z3git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_v1_capability_proto_rawDescOnce sync.Once
|
file_v1_capability_proto_rawDescOnce sync.Once
|
||||||
@ -8979,7 +9428,7 @@ func file_v1_capability_proto_rawDescGZIP() []byte {
|
|||||||
return file_v1_capability_proto_rawDescData
|
return file_v1_capability_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_v1_capability_proto_msgTypes = make([]protoimpl.MessageInfo, 161)
|
var file_v1_capability_proto_msgTypes = make([]protoimpl.MessageInfo, 168)
|
||||||
var file_v1_capability_proto_goTypes = []any{
|
var file_v1_capability_proto_goTypes = []any{
|
||||||
(*HostCallRequest)(nil), // 0: abi.v1.HostCallRequest
|
(*HostCallRequest)(nil), // 0: abi.v1.HostCallRequest
|
||||||
(*HostCallResponse)(nil), // 1: abi.v1.HostCallResponse
|
(*HostCallResponse)(nil), // 1: abi.v1.HostCallResponse
|
||||||
@ -9138,21 +9587,28 @@ var file_v1_capability_proto_goTypes = []any{
|
|||||||
(*BridgeInvokeResponse)(nil), // 154: abi.v1.BridgeInvokeResponse
|
(*BridgeInvokeResponse)(nil), // 154: abi.v1.BridgeInvokeResponse
|
||||||
(*HttpRequestRequest)(nil), // 155: abi.v1.HttpRequestRequest
|
(*HttpRequestRequest)(nil), // 155: abi.v1.HttpRequestRequest
|
||||||
(*HttpRequestResponse)(nil), // 156: abi.v1.HttpRequestResponse
|
(*HttpRequestResponse)(nil), // 156: abi.v1.HttpRequestResponse
|
||||||
nil, // 157: abi.v1.AuthorProfile.SocialLinksEntry
|
(*FileAccessGrant)(nil), // 157: abi.v1.FileAccessGrant
|
||||||
nil, // 158: abi.v1.RagResult.MetadataEntry
|
(*FilesGrantAccessRequest)(nil), // 158: abi.v1.FilesGrantAccessRequest
|
||||||
nil, // 159: abi.v1.HttpRequestRequest.HeadersEntry
|
(*FilesGrantAccessResponse)(nil), // 159: abi.v1.FilesGrantAccessResponse
|
||||||
nil, // 160: abi.v1.HttpRequestResponse.HeadersEntry
|
(*FilesRevokeAccessRequest)(nil), // 160: abi.v1.FilesRevokeAccessRequest
|
||||||
(*AbiError)(nil), // 161: abi.v1.AbiError
|
(*FilesRevokeAccessResponse)(nil), // 161: abi.v1.FilesRevokeAccessResponse
|
||||||
(*timestamppb.Timestamp)(nil), // 162: google.protobuf.Timestamp
|
(*FilesListGrantsRequest)(nil), // 162: abi.v1.FilesListGrantsRequest
|
||||||
(*HeaderValues)(nil), // 163: abi.v1.HeaderValues
|
(*FilesListGrantsResponse)(nil), // 163: abi.v1.FilesListGrantsResponse
|
||||||
|
nil, // 164: abi.v1.AuthorProfile.SocialLinksEntry
|
||||||
|
nil, // 165: abi.v1.RagResult.MetadataEntry
|
||||||
|
nil, // 166: abi.v1.HttpRequestRequest.HeadersEntry
|
||||||
|
nil, // 167: abi.v1.HttpRequestResponse.HeadersEntry
|
||||||
|
(*AbiError)(nil), // 168: abi.v1.AbiError
|
||||||
|
(*timestamppb.Timestamp)(nil), // 169: google.protobuf.Timestamp
|
||||||
|
(*HeaderValues)(nil), // 170: abi.v1.HeaderValues
|
||||||
}
|
}
|
||||||
var file_v1_capability_proto_depIdxs = []int32{
|
var file_v1_capability_proto_depIdxs = []int32{
|
||||||
161, // 0: abi.v1.HostCallResponse.error:type_name -> abi.v1.AbiError
|
168, // 0: abi.v1.HostCallResponse.error:type_name -> abi.v1.AbiError
|
||||||
4, // 1: abi.v1.ContentGetAuthorProfileResponse.author:type_name -> abi.v1.AuthorProfile
|
4, // 1: abi.v1.ContentGetAuthorProfileResponse.author:type_name -> abi.v1.AuthorProfile
|
||||||
157, // 2: abi.v1.AuthorProfile.social_links:type_name -> abi.v1.AuthorProfile.SocialLinksEntry
|
164, // 2: abi.v1.AuthorProfile.social_links:type_name -> abi.v1.AuthorProfile.SocialLinksEntry
|
||||||
7, // 3: abi.v1.ContentGetPageResponse.page:type_name -> abi.v1.PageInfo
|
7, // 3: abi.v1.ContentGetPageResponse.page:type_name -> abi.v1.PageInfo
|
||||||
12, // 4: abi.v1.ContentGetPostResponse.post:type_name -> abi.v1.PostInfo
|
12, // 4: abi.v1.ContentGetPostResponse.post:type_name -> abi.v1.PostInfo
|
||||||
162, // 5: abi.v1.PostInfo.published_at:type_name -> google.protobuf.Timestamp
|
169, // 5: abi.v1.PostInfo.published_at:type_name -> google.protobuf.Timestamp
|
||||||
12, // 6: abi.v1.ContentListPostsResponse.posts:type_name -> abi.v1.PostInfo
|
12, // 6: abi.v1.ContentListPostsResponse.posts:type_name -> abi.v1.PostInfo
|
||||||
33, // 7: abi.v1.GatingEvaluateAccessRequest.rule:type_name -> abi.v1.AccessRule
|
33, // 7: abi.v1.GatingEvaluateAccessRequest.rule:type_name -> abi.v1.AccessRule
|
||||||
34, // 8: abi.v1.GatingEvaluateAccessResponse.result:type_name -> abi.v1.AccessResult
|
34, // 8: abi.v1.GatingEvaluateAccessResponse.result:type_name -> abi.v1.AccessResult
|
||||||
@ -9172,21 +9628,27 @@ var file_v1_capability_proto_depIdxs = []int32{
|
|||||||
74, // 22: abi.v1.SubscriptionsGetTierBySlugResponse.tier:type_name -> abi.v1.Tier
|
74, // 22: abi.v1.SubscriptionsGetTierBySlugResponse.tier:type_name -> abi.v1.Tier
|
||||||
74, // 23: abi.v1.SubscriptionsListTiersResponse.tiers:type_name -> abi.v1.Tier
|
74, // 23: abi.v1.SubscriptionsListTiersResponse.tiers:type_name -> abi.v1.Tier
|
||||||
79, // 24: abi.v1.SubscriptionsListActivePlansResponse.plans:type_name -> abi.v1.Plan
|
79, // 24: abi.v1.SubscriptionsListActivePlansResponse.plans:type_name -> abi.v1.Plan
|
||||||
162, // 25: abi.v1.Plan.created_at:type_name -> google.protobuf.Timestamp
|
169, // 25: abi.v1.Plan.created_at:type_name -> google.protobuf.Timestamp
|
||||||
102, // 26: abi.v1.RagQueryResponse.results:type_name -> abi.v1.RagResult
|
102, // 26: abi.v1.RagQueryResponse.results:type_name -> abi.v1.RagResult
|
||||||
158, // 27: abi.v1.RagResult.metadata:type_name -> abi.v1.RagResult.MetadataEntry
|
165, // 27: abi.v1.RagResult.metadata:type_name -> abi.v1.RagResult.MetadataEntry
|
||||||
111, // 28: abi.v1.ContentSetPageBlocksRequest.blocks:type_name -> abi.v1.PageBlock
|
111, // 28: abi.v1.ContentSetPageBlocksRequest.blocks:type_name -> abi.v1.PageBlock
|
||||||
111, // 29: abi.v1.PageSeed.blocks:type_name -> abi.v1.PageBlock
|
111, // 29: abi.v1.PageSeed.blocks:type_name -> abi.v1.PageBlock
|
||||||
126, // 30: abi.v1.ProvisionerEnsurePageRequest.page:type_name -> abi.v1.PageSeed
|
126, // 30: abi.v1.ProvisionerEnsurePageRequest.page:type_name -> abi.v1.PageSeed
|
||||||
159, // 31: abi.v1.HttpRequestRequest.headers:type_name -> abi.v1.HttpRequestRequest.HeadersEntry
|
166, // 31: abi.v1.HttpRequestRequest.headers:type_name -> abi.v1.HttpRequestRequest.HeadersEntry
|
||||||
160, // 32: abi.v1.HttpRequestResponse.headers:type_name -> abi.v1.HttpRequestResponse.HeadersEntry
|
167, // 32: abi.v1.HttpRequestResponse.headers:type_name -> abi.v1.HttpRequestResponse.HeadersEntry
|
||||||
163, // 33: abi.v1.HttpRequestRequest.HeadersEntry.value:type_name -> abi.v1.HeaderValues
|
169, // 33: abi.v1.FileAccessGrant.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
163, // 34: abi.v1.HttpRequestResponse.HeadersEntry.value:type_name -> abi.v1.HeaderValues
|
169, // 34: abi.v1.FileAccessGrant.revoked_at:type_name -> google.protobuf.Timestamp
|
||||||
35, // [35:35] is the sub-list for method output_type
|
169, // 35: abi.v1.FileAccessGrant.created_at:type_name -> google.protobuf.Timestamp
|
||||||
35, // [35:35] is the sub-list for method input_type
|
169, // 36: abi.v1.FilesGrantAccessRequest.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
35, // [35:35] is the sub-list for extension type_name
|
157, // 37: abi.v1.FilesGrantAccessResponse.grant:type_name -> abi.v1.FileAccessGrant
|
||||||
35, // [35:35] is the sub-list for extension extendee
|
157, // 38: abi.v1.FilesListGrantsResponse.grants:type_name -> abi.v1.FileAccessGrant
|
||||||
0, // [0:35] is the sub-list for field type_name
|
170, // 39: abi.v1.HttpRequestRequest.HeadersEntry.value:type_name -> abi.v1.HeaderValues
|
||||||
|
170, // 40: abi.v1.HttpRequestResponse.HeadersEntry.value:type_name -> abi.v1.HeaderValues
|
||||||
|
41, // [41:41] is the sub-list for method output_type
|
||||||
|
41, // [41:41] is the sub-list for method input_type
|
||||||
|
41, // [41:41] is the sub-list for extension type_name
|
||||||
|
41, // [41:41] is the sub-list for extension extendee
|
||||||
|
0, // [0:41] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_v1_capability_proto_init() }
|
func init() { file_v1_capability_proto_init() }
|
||||||
@ -9206,7 +9668,7 @@ func file_v1_capability_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_capability_proto_rawDesc), len(file_v1_capability_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_capability_proto_rawDesc), len(file_v1_capability_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 161,
|
NumMessages: 168,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
# A plugin grants and revokes file access through the files family
|
||||||
|
|
||||||
|
Status: accepted (WO-FL-009, 2026-09-17)
|
||||||
|
|
||||||
|
The CMS File library gates a published file and hands it to one person at a time
|
||||||
|
through a File access grant (cms ADRs 0181, 0186, 0187). ADR 0181 named the
|
||||||
|
storefront case explicitly: selling a single file belongs to a plugin, which
|
||||||
|
creates a grant after payment and revokes it on a refund. Until now a plugin had
|
||||||
|
no way to say either thing, because the capability surface carried no file
|
||||||
|
family at all.
|
||||||
|
|
||||||
|
Decision: the ABI gains `files.grant_access`, `files.revoke_access` and
|
||||||
|
`files.list_grants`, mirrored by `plugin.FileGrants` on `CoreServices`. A grant
|
||||||
|
names a file by its attachment kind and id and exactly one subject: a public
|
||||||
|
user, or an email address that receives a signed unlock link instead. The
|
||||||
|
response carries the stored grant, including that link, so a plugin that sells a
|
||||||
|
file to somebody with no account can deliver it in its own receipt.
|
||||||
|
|
||||||
|
**The source is the host's to write, and so is the revoke rule.** Nothing in
|
||||||
|
these messages names the calling plugin: the host already authenticates the
|
||||||
|
caller at the capability boundary, stamps `plugin:<name>` on every grant this
|
||||||
|
family writes, and refuses a revoke of a grant with any other source. A plugin
|
||||||
|
therefore cannot attribute a grant to somebody else, and cannot take away access
|
||||||
|
that an administrator, a workflow or another plugin gave. Passing the plugin
|
||||||
|
name on the wire was rejected for the same reason the bridge does not: a value a
|
||||||
|
guest supplies is a value a guest can change.
|
||||||
|
|
||||||
|
A grant with no expiry is the ordinary case, because revocation is the control
|
||||||
|
the CMS relies on; `expires_at` is optional and absent means "until revoked".
|
||||||
|
|
||||||
|
Alternatives rejected: a general "write a row in a core table" capability, which
|
||||||
|
would put the CHECK constraints and the unique indexes of a security table
|
||||||
|
behind a generic escape hatch; and returning only a grant id, which would force
|
||||||
|
a second call for the link every seller needs.
|
||||||
|
|
||||||
|
Consequences:
|
||||||
|
|
||||||
|
- New: `abi/proto/v1/capability.proto` messages `FileAccessGrant`,
|
||||||
|
`FilesGrantAccessRequest`/`Response`, `FilesRevokeAccessRequest`/`Response`,
|
||||||
|
`FilesListGrantsRequest`/`Response`; `plugin/wasmguest/caps/files.go` and its
|
||||||
|
round-trip goldens; `plugin.FileGrants`, `plugin.FileGrantParams` and
|
||||||
|
`plugin.FileGrant` in `plugin/deps.go`.
|
||||||
|
- `CoreServices.FileGrants` is nil on a host that does not wire it, exactly like
|
||||||
|
the other optional members, and the guest stub then fails the call rather than
|
||||||
|
pretending.
|
||||||
|
- The host half, the plugin attribution and the revoke guard live in the cms
|
||||||
|
repo (`backend/plugin/wasmhost/caps/files.go`, ADR 0190 there).
|
||||||
|
|
||||||
|
Keywords: files.grant_access, files.revoke_access, files.list_grants,
|
||||||
|
FileGrants, FileGrantParams, FileGrant, FileAccessGrant, FilesGrantAccessRequest,
|
||||||
|
FilesRevokeAccessRequest, FilesListGrantsRequest, filesStub, capability.proto,
|
||||||
|
File access grant, unlock link, unlock_url, plugin grant, grant source,
|
||||||
|
storefront plugin, File purchase, revoke own grants, WO-FL-009, ADR 0181,
|
||||||
|
CoreServices, wasm ABI, host_call
|
||||||
@ -3,6 +3,7 @@ package plugin
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"git.dev.alexdunmow.com/block/pluginsdk/ai"
|
"git.dev.alexdunmow.com/block/pluginsdk/ai"
|
||||||
@ -67,6 +68,7 @@ type CoreServices struct {
|
|||||||
ReviewSubmitter ReviewSubmitter
|
ReviewSubmitter ReviewSubmitter
|
||||||
BadgeRefresher BadgeRefresher
|
BadgeRefresher BadgeRefresher
|
||||||
SettingsUpdater settings.Updater
|
SettingsUpdater settings.Updater
|
||||||
|
FileGrants FileGrants
|
||||||
|
|
||||||
// Extension points — typed as narrow interfaces where possible
|
// Extension points — typed as narrow interfaces where possible
|
||||||
JobRunner JobRunner
|
JobRunner JobRunner
|
||||||
@ -149,6 +151,46 @@ type RAGResult struct {
|
|||||||
Metadata map[string]string
|
Metadata map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileGrants hands one File library file to one person and takes it back. A
|
||||||
|
// grant is "this person may download this file whatever tier they hold", which
|
||||||
|
// is how a storefront plugin delivers a purchase and withdraws it on a refund.
|
||||||
|
// The host attributes every grant written here to the calling plugin, and a
|
||||||
|
// plugin may revoke only the grants it wrote.
|
||||||
|
type FileGrants interface {
|
||||||
|
GrantFileAccess(ctx context.Context, params FileGrantParams) (FileGrant, error)
|
||||||
|
RevokeFileAccess(ctx context.Context, grantID uuid.UUID) error
|
||||||
|
ListFileGrants(ctx context.Context, attachmentKind string, attachmentID uuid.UUID) ([]FileGrant, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileGrantParams names the file and exactly one subject: a public user, or an
|
||||||
|
// email address that receives a signed unlock link instead.
|
||||||
|
type FileGrantParams struct {
|
||||||
|
// AttachmentKind is "library" or "table"; empty means "library".
|
||||||
|
AttachmentKind string
|
||||||
|
AttachmentID uuid.UUID
|
||||||
|
PublicUserID uuid.UUID
|
||||||
|
Email string
|
||||||
|
// ExpiresAt zero means the grant ends only when it is revoked.
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileGrant is one stored grant. A revoked grant is kept as the record of who
|
||||||
|
// was let in and when that stopped.
|
||||||
|
type FileGrant struct {
|
||||||
|
GrantID uuid.UUID
|
||||||
|
AttachmentKind string
|
||||||
|
AttachmentID uuid.UUID
|
||||||
|
PublicUserID uuid.UUID
|
||||||
|
Email string
|
||||||
|
Source string
|
||||||
|
ExpiresAt time.Time
|
||||||
|
RevokedAt time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
// UnlockURL opens the file for this grant. It is empty when the file has no
|
||||||
|
// permanent link to sign.
|
||||||
|
UnlockURL string
|
||||||
|
}
|
||||||
|
|
||||||
// BadgeRefresher recomputes badges for a data table row.
|
// BadgeRefresher recomputes badges for a data table row.
|
||||||
// The CMS handles loading the table schema, aggregating ratings,
|
// The CMS handles loading the table schema, aggregating ratings,
|
||||||
// evaluating badge rules, and persisting the updated badge list.
|
// evaluating badge rules, and persisting the updated badge list.
|
||||||
|
|||||||
@ -711,6 +711,45 @@ func TestCapabilityRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// --- files (WO-FL-009) ---
|
||||||
|
{
|
||||||
|
name: "files_grant_access", wantMethod: "files.grant_access",
|
||||||
|
resp: &abiv1.FilesGrantAccessResponse{Grant: &abiv1.FileAccessGrant{
|
||||||
|
GrantId: idItem.String(), AttachmentKind: "library", AttachmentId: idRow.String(),
|
||||||
|
Email: "reader@example.test", Source: "plugin:testplugin",
|
||||||
|
UnlockUrl: "https://example.test/files/l/abcdefghjkmnpqrs/guide?unlock=t",
|
||||||
|
}},
|
||||||
|
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||||
|
got, err := cs.FileGrants.GrantFileAccess(ctx, plugin.FileGrantParams{
|
||||||
|
AttachmentKind: "library", AttachmentID: idRow, Email: "reader@example.test",
|
||||||
|
})
|
||||||
|
if err != nil || got.GrantID != idItem || got.Source != "plugin:testplugin" || got.UnlockURL == "" {
|
||||||
|
t.Errorf("grant = %+v err = %v", got, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "files_revoke_access", wantMethod: "files.revoke_access",
|
||||||
|
resp: &abiv1.FilesRevokeAccessResponse{},
|
||||||
|
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||||
|
if err := cs.FileGrants.RevokeFileAccess(ctx, idItem); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "files_list_grants", wantMethod: "files.list_grants",
|
||||||
|
resp: &abiv1.FilesListGrantsResponse{Grants: []*abiv1.FileAccessGrant{{
|
||||||
|
GrantId: idItem.String(), AttachmentKind: "library", AttachmentId: idRow.String(),
|
||||||
|
PublicUserId: idUser.String(), Source: "plugin:testplugin",
|
||||||
|
}}},
|
||||||
|
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||||
|
got, err := cs.FileGrants.ListFileGrants(ctx, "library", idRow)
|
||||||
|
if err != nil || len(got) != 1 || got[0].PublicUserID != idUser {
|
||||||
|
t.Errorf("grants = %+v err = %v", got, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
// --- content writes (WO-WZ-019) ---
|
// --- content writes (WO-WZ-019) ---
|
||||||
{
|
{
|
||||||
name: "content_create_page", wantMethod: "content.create_page",
|
name: "content_create_page", wantMethod: "content.create_page",
|
||||||
|
|||||||
@ -44,6 +44,7 @@ func NewCoreServices(call CallFunc) plugin.CoreServices {
|
|||||||
Bridge: &bridgeStub{base: base{family: "bridge", call: call}},
|
Bridge: &bridgeStub{base: base{family: "bridge", call: call}},
|
||||||
ReviewSubmitter: &reviewsStub{base{family: "reviews", call: call}},
|
ReviewSubmitter: &reviewsStub{base{family: "reviews", call: call}},
|
||||||
BadgeRefresher: &badgesStub{base{family: "badges", call: call}},
|
BadgeRefresher: &badgesStub{base{family: "badges", call: call}},
|
||||||
|
FileGrants: &filesStub{base{family: "files", call: call}},
|
||||||
JobRunner: &jobsStub{base{family: "jobs", call: call}},
|
JobRunner: &jobsStub{base{family: "jobs", call: call}},
|
||||||
EmbeddingService: &embeddingsStub{base{family: "embeddings", call: call}},
|
EmbeddingService: &embeddingsStub{base{family: "embeddings", call: call}},
|
||||||
RAGService: NewRAGStub(call),
|
RAGService: NewRAGStub(call),
|
||||||
|
|||||||
79
plugin/wasmguest/caps/files.go
Normal file
79
plugin/wasmguest/caps/files.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
2
plugin/wasmguest/caps/testdata/golden/files_grant_access_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/files_grant_access_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
library$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"reader@example.test
|
||||||
3
plugin/wasmguest/caps/testdata/golden/files_grant_access_resp.pb
vendored
Normal file
3
plugin/wasmguest/caps/testdata/golden/files_grant_access_resp.pb
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
»
|
||||||
|
$44444444-4444-4444-4444-444444444444library$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb*reader@example.test2plugin:testpluginR<https://example.test/files/l/abcdefghjkmnpqrs/guide?unlock=t
|
||||||
2
plugin/wasmguest/caps/testdata/golden/files_list_grants_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/files_list_grants_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
library$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
|
||||||
3
plugin/wasmguest/caps/testdata/golden/files_list_grants_resp.pb
vendored
Normal file
3
plugin/wasmguest/caps/testdata/golden/files_list_grants_resp.pb
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
Ž
|
||||||
|
$44444444-4444-4444-4444-444444444444library$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"$11111111-1111-1111-1111-1111111111112plugin:testplugin
|
||||||
2
plugin/wasmguest/caps/testdata/golden/files_revoke_access_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/files_revoke_access_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
$44444444-4444-4444-4444-444444444444
|
||||||
0
plugin/wasmguest/caps/testdata/golden/files_revoke_access_resp.pb
vendored
Normal file
0
plugin/wasmguest/caps/testdata/golden/files_revoke_access_resp.pb
vendored
Normal file
Loading…
x
Reference in New Issue
Block a user