mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-31 14:06:20 +00:00
filer: add extended-attribute guard clauses for object-lock (#9648)
Routing object-lock buckets off the distributed lock needs the retention and legal-hold check to run atomically with the write, under the per-path lock. Move just the comparison into the filer, not the S3 semantics: two generic clause kinds on an extended attribute. IF_EXTENDED_NOT_EQUAL blocks while extended[ext_key] equals ext_value (a legal hold). IF_EXTENDED_TIME_ELAPSED blocks while extended[ext_key], read as a unix- second deadline, is in the future against the filer's clock (retention); a malformed deadline fails safe. The caller composes these from the object-lock state and, for a governance bypass, simply omits the retention clause once the bypass is authorized -- the filer makes no authorization decision and keeps no S3 knowledge.
This commit is contained in:
@@ -242,22 +242,35 @@ message CreateEntryRequest {
|
||||
// compound conditions (e.g. If-Match + If-Unmodified-Since together).
|
||||
message WriteCondition {
|
||||
enum Kind {
|
||||
NONE = 0; // unconditional
|
||||
IF_NOT_EXISTS = 1; // fail if the entry exists (If-None-Match: *)
|
||||
IF_EXISTS = 2; // fail if the entry is absent (If-Match: *)
|
||||
IF_ETAG_MATCH = 3; // fail if absent or etag matches none of the set (If-Match)
|
||||
IF_ETAG_NOT_MATCH = 4; // fail if present and etag matches any of the set (If-None-Match)
|
||||
IF_UNMODIFIED_SINCE = 5; // fail if present and mtime > unix_time
|
||||
IF_MODIFIED_SINCE = 6; // fail if present and mtime <= unix_time
|
||||
NONE = 0; // unconditional
|
||||
IF_NOT_EXISTS = 1; // fail if the entry exists (If-None-Match: *)
|
||||
IF_EXISTS = 2; // fail if the entry is absent (If-Match: *)
|
||||
IF_ETAG_MATCH = 3; // fail if absent or etag matches none of the set (If-Match)
|
||||
IF_ETAG_NOT_MATCH = 4; // fail if present and etag matches any of the set (If-None-Match)
|
||||
IF_UNMODIFIED_SINCE = 5; // fail if present and mtime > unix_time
|
||||
IF_MODIFIED_SINCE = 6; // fail if present and mtime <= unix_time
|
||||
IF_EXTENDED_NOT_EQUAL = 7; // fail if present and extended[ext_key] == ext_value
|
||||
IF_EXTENDED_TIME_ELAPSED = 8; // fail if present and extended[ext_key] (unix seconds) is in the future
|
||||
}
|
||||
// Clause is one primitive comparison. IF_ETAG_MATCH holds when the current
|
||||
// entry's ETag equals any value in etags; IF_ETAG_NOT_MATCH holds when it
|
||||
// equals none. allow_weak permits weak-comparison (ignoring the W/ prefix).
|
||||
//
|
||||
// The IF_EXTENDED_* kinds are generic guards on an extended attribute, used
|
||||
// to enforce object-lock without teaching the filer S3 semantics:
|
||||
// IF_EXTENDED_NOT_EQUAL expresses a legal hold (block while a key equals a
|
||||
// value), and IF_EXTENDED_TIME_ELAPSED expresses retention (block while a
|
||||
// stored unix-second deadline is in the future, compared to the filer's
|
||||
// clock). The caller composes these and, for governance-bypass, simply omits
|
||||
// the retention clause when the bypass is authorized — the filer makes no
|
||||
// authorization decision.
|
||||
message Clause {
|
||||
Kind kind = 1;
|
||||
repeated string etags = 2; // ETag set for IF_ETAG_* kinds
|
||||
int64 unix_time = 3; // bound (unix seconds) for IF_*_SINCE kinds
|
||||
bool allow_weak = 4; // compare ETags ignoring the weak (W/) marker
|
||||
string ext_key = 5; // extended attribute name for IF_EXTENDED_* kinds
|
||||
string ext_value = 6; // blocking value for IF_EXTENDED_NOT_EQUAL
|
||||
}
|
||||
repeated Clause clauses = 1; // all must hold (logical AND)
|
||||
}
|
||||
|
||||
@@ -242,22 +242,35 @@ message CreateEntryRequest {
|
||||
// compound conditions (e.g. If-Match + If-Unmodified-Since together).
|
||||
message WriteCondition {
|
||||
enum Kind {
|
||||
NONE = 0; // unconditional
|
||||
IF_NOT_EXISTS = 1; // fail if the entry exists (If-None-Match: *)
|
||||
IF_EXISTS = 2; // fail if the entry is absent (If-Match: *)
|
||||
IF_ETAG_MATCH = 3; // fail if absent or etag matches none of the set (If-Match)
|
||||
IF_ETAG_NOT_MATCH = 4; // fail if present and etag matches any of the set (If-None-Match)
|
||||
IF_UNMODIFIED_SINCE = 5; // fail if present and mtime > unix_time
|
||||
IF_MODIFIED_SINCE = 6; // fail if present and mtime <= unix_time
|
||||
NONE = 0; // unconditional
|
||||
IF_NOT_EXISTS = 1; // fail if the entry exists (If-None-Match: *)
|
||||
IF_EXISTS = 2; // fail if the entry is absent (If-Match: *)
|
||||
IF_ETAG_MATCH = 3; // fail if absent or etag matches none of the set (If-Match)
|
||||
IF_ETAG_NOT_MATCH = 4; // fail if present and etag matches any of the set (If-None-Match)
|
||||
IF_UNMODIFIED_SINCE = 5; // fail if present and mtime > unix_time
|
||||
IF_MODIFIED_SINCE = 6; // fail if present and mtime <= unix_time
|
||||
IF_EXTENDED_NOT_EQUAL = 7; // fail if present and extended[ext_key] == ext_value
|
||||
IF_EXTENDED_TIME_ELAPSED = 8; // fail if present and extended[ext_key] (unix seconds) is in the future
|
||||
}
|
||||
// Clause is one primitive comparison. IF_ETAG_MATCH holds when the current
|
||||
// entry's ETag equals any value in etags; IF_ETAG_NOT_MATCH holds when it
|
||||
// equals none. allow_weak permits weak-comparison (ignoring the W/ prefix).
|
||||
//
|
||||
// The IF_EXTENDED_* kinds are generic guards on an extended attribute, used
|
||||
// to enforce object-lock without teaching the filer S3 semantics:
|
||||
// IF_EXTENDED_NOT_EQUAL expresses a legal hold (block while a key equals a
|
||||
// value), and IF_EXTENDED_TIME_ELAPSED expresses retention (block while a
|
||||
// stored unix-second deadline is in the future, compared to the filer's
|
||||
// clock). The caller composes these and, for governance-bypass, simply omits
|
||||
// the retention clause when the bypass is authorized — the filer makes no
|
||||
// authorization decision.
|
||||
message Clause {
|
||||
Kind kind = 1;
|
||||
repeated string etags = 2; // ETag set for IF_ETAG_* kinds
|
||||
int64 unix_time = 3; // bound (unix seconds) for IF_*_SINCE kinds
|
||||
bool allow_weak = 4; // compare ETags ignoring the weak (W/) marker
|
||||
string ext_key = 5; // extended attribute name for IF_EXTENDED_* kinds
|
||||
string ext_value = 6; // blocking value for IF_EXTENDED_NOT_EQUAL
|
||||
}
|
||||
repeated Clause clauses = 1; // all must hold (logical AND)
|
||||
}
|
||||
|
||||
@@ -139,13 +139,15 @@ func (FilerError) EnumDescriptor() ([]byte, []int) {
|
||||
type WriteCondition_Kind int32
|
||||
|
||||
const (
|
||||
WriteCondition_NONE WriteCondition_Kind = 0 // unconditional
|
||||
WriteCondition_IF_NOT_EXISTS WriteCondition_Kind = 1 // fail if the entry exists (If-None-Match: *)
|
||||
WriteCondition_IF_EXISTS WriteCondition_Kind = 2 // fail if the entry is absent (If-Match: *)
|
||||
WriteCondition_IF_ETAG_MATCH WriteCondition_Kind = 3 // fail if absent or etag matches none of the set (If-Match)
|
||||
WriteCondition_IF_ETAG_NOT_MATCH WriteCondition_Kind = 4 // fail if present and etag matches any of the set (If-None-Match)
|
||||
WriteCondition_IF_UNMODIFIED_SINCE WriteCondition_Kind = 5 // fail if present and mtime > unix_time
|
||||
WriteCondition_IF_MODIFIED_SINCE WriteCondition_Kind = 6 // fail if present and mtime <= unix_time
|
||||
WriteCondition_NONE WriteCondition_Kind = 0 // unconditional
|
||||
WriteCondition_IF_NOT_EXISTS WriteCondition_Kind = 1 // fail if the entry exists (If-None-Match: *)
|
||||
WriteCondition_IF_EXISTS WriteCondition_Kind = 2 // fail if the entry is absent (If-Match: *)
|
||||
WriteCondition_IF_ETAG_MATCH WriteCondition_Kind = 3 // fail if absent or etag matches none of the set (If-Match)
|
||||
WriteCondition_IF_ETAG_NOT_MATCH WriteCondition_Kind = 4 // fail if present and etag matches any of the set (If-None-Match)
|
||||
WriteCondition_IF_UNMODIFIED_SINCE WriteCondition_Kind = 5 // fail if present and mtime > unix_time
|
||||
WriteCondition_IF_MODIFIED_SINCE WriteCondition_Kind = 6 // fail if present and mtime <= unix_time
|
||||
WriteCondition_IF_EXTENDED_NOT_EQUAL WriteCondition_Kind = 7 // fail if present and extended[ext_key] == ext_value
|
||||
WriteCondition_IF_EXTENDED_TIME_ELAPSED WriteCondition_Kind = 8 // fail if present and extended[ext_key] (unix seconds) is in the future
|
||||
)
|
||||
|
||||
// Enum value maps for WriteCondition_Kind.
|
||||
@@ -158,15 +160,19 @@ var (
|
||||
4: "IF_ETAG_NOT_MATCH",
|
||||
5: "IF_UNMODIFIED_SINCE",
|
||||
6: "IF_MODIFIED_SINCE",
|
||||
7: "IF_EXTENDED_NOT_EQUAL",
|
||||
8: "IF_EXTENDED_TIME_ELAPSED",
|
||||
}
|
||||
WriteCondition_Kind_value = map[string]int32{
|
||||
"NONE": 0,
|
||||
"IF_NOT_EXISTS": 1,
|
||||
"IF_EXISTS": 2,
|
||||
"IF_ETAG_MATCH": 3,
|
||||
"IF_ETAG_NOT_MATCH": 4,
|
||||
"IF_UNMODIFIED_SINCE": 5,
|
||||
"IF_MODIFIED_SINCE": 6,
|
||||
"NONE": 0,
|
||||
"IF_NOT_EXISTS": 1,
|
||||
"IF_EXISTS": 2,
|
||||
"IF_ETAG_MATCH": 3,
|
||||
"IF_ETAG_NOT_MATCH": 4,
|
||||
"IF_UNMODIFIED_SINCE": 5,
|
||||
"IF_MODIFIED_SINCE": 6,
|
||||
"IF_EXTENDED_NOT_EQUAL": 7,
|
||||
"IF_EXTENDED_TIME_ELAPSED": 8,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5771,12 +5777,23 @@ func (x *MountInfo) GetDataCenter() string {
|
||||
// Clause is one primitive comparison. IF_ETAG_MATCH holds when the current
|
||||
// entry's ETag equals any value in etags; IF_ETAG_NOT_MATCH holds when it
|
||||
// equals none. allow_weak permits weak-comparison (ignoring the W/ prefix).
|
||||
//
|
||||
// The IF_EXTENDED_* kinds are generic guards on an extended attribute, used
|
||||
// to enforce object-lock without teaching the filer S3 semantics:
|
||||
// IF_EXTENDED_NOT_EQUAL expresses a legal hold (block while a key equals a
|
||||
// value), and IF_EXTENDED_TIME_ELAPSED expresses retention (block while a
|
||||
// stored unix-second deadline is in the future, compared to the filer's
|
||||
// clock). The caller composes these and, for governance-bypass, simply omits
|
||||
// the retention clause when the bypass is authorized — the filer makes no
|
||||
// authorization decision.
|
||||
type WriteCondition_Clause struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Kind WriteCondition_Kind `protobuf:"varint,1,opt,name=kind,proto3,enum=filer_pb.WriteCondition_Kind" json:"kind,omitempty"`
|
||||
Etags []string `protobuf:"bytes,2,rep,name=etags,proto3" json:"etags,omitempty"` // ETag set for IF_ETAG_* kinds
|
||||
UnixTime int64 `protobuf:"varint,3,opt,name=unix_time,json=unixTime,proto3" json:"unix_time,omitempty"` // bound (unix seconds) for IF_*_SINCE kinds
|
||||
AllowWeak bool `protobuf:"varint,4,opt,name=allow_weak,json=allowWeak,proto3" json:"allow_weak,omitempty"` // compare ETags ignoring the weak (W/) marker
|
||||
ExtKey string `protobuf:"bytes,5,opt,name=ext_key,json=extKey,proto3" json:"ext_key,omitempty"` // extended attribute name for IF_EXTENDED_* kinds
|
||||
ExtValue string `protobuf:"bytes,6,opt,name=ext_value,json=extValue,proto3" json:"ext_value,omitempty"` // blocking value for IF_EXTENDED_NOT_EQUAL
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -5839,6 +5856,20 @@ func (x *WriteCondition_Clause) GetAllowWeak() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *WriteCondition_Clause) GetExtKey() string {
|
||||
if x != nil {
|
||||
return x.ExtKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *WriteCondition_Clause) GetExtValue() string {
|
||||
if x != nil {
|
||||
return x.ExtValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// if found, send the exact address
|
||||
// if not found, send the full list of existing brokers
|
||||
type LocateBrokerResponse_Resource struct {
|
||||
@@ -6171,15 +6202,17 @@ const file_filer_proto_rawDesc = "" +
|
||||
"signatures\x18\x05 \x03(\x05R\n" +
|
||||
"signatures\x12=\n" +
|
||||
"\x1bskip_check_parent_directory\x18\x06 \x01(\bR\x18skipCheckParentDirectory\x126\n" +
|
||||
"\tcondition\x18\a \x01(\v2\x18.filer_pb.WriteConditionR\tcondition\"\xea\x02\n" +
|
||||
"\tcondition\x18\a \x01(\v2\x18.filer_pb.WriteConditionR\tcondition\"\xd9\x03\n" +
|
||||
"\x0eWriteCondition\x129\n" +
|
||||
"\aclauses\x18\x01 \x03(\v2\x1f.filer_pb.WriteCondition.ClauseR\aclauses\x1a\x8d\x01\n" +
|
||||
"\aclauses\x18\x01 \x03(\v2\x1f.filer_pb.WriteCondition.ClauseR\aclauses\x1a\xc3\x01\n" +
|
||||
"\x06Clause\x121\n" +
|
||||
"\x04kind\x18\x01 \x01(\x0e2\x1d.filer_pb.WriteCondition.KindR\x04kind\x12\x14\n" +
|
||||
"\x05etags\x18\x02 \x03(\tR\x05etags\x12\x1b\n" +
|
||||
"\tunix_time\x18\x03 \x01(\x03R\bunixTime\x12\x1d\n" +
|
||||
"\n" +
|
||||
"allow_weak\x18\x04 \x01(\bR\tallowWeak\"\x8c\x01\n" +
|
||||
"allow_weak\x18\x04 \x01(\bR\tallowWeak\x12\x17\n" +
|
||||
"\aext_key\x18\x05 \x01(\tR\x06extKey\x12\x1b\n" +
|
||||
"\text_value\x18\x06 \x01(\tR\bextValue\"\xc5\x01\n" +
|
||||
"\x04Kind\x12\b\n" +
|
||||
"\x04NONE\x10\x00\x12\x11\n" +
|
||||
"\rIF_NOT_EXISTS\x10\x01\x12\r\n" +
|
||||
@@ -6187,7 +6220,9 @@ const file_filer_proto_rawDesc = "" +
|
||||
"\rIF_ETAG_MATCH\x10\x03\x12\x15\n" +
|
||||
"\x11IF_ETAG_NOT_MATCH\x10\x04\x12\x17\n" +
|
||||
"\x13IF_UNMODIFIED_SINCE\x10\x05\x12\x15\n" +
|
||||
"\x11IF_MODIFIED_SINCE\x10\x06\"\x96\x04\n" +
|
||||
"\x11IF_MODIFIED_SINCE\x10\x06\x12\x19\n" +
|
||||
"\x15IF_EXTENDED_NOT_EQUAL\x10\a\x12\x1c\n" +
|
||||
"\x18IF_EXTENDED_TIME_ELAPSED\x10\b\"\x96\x04\n" +
|
||||
"\x0eObjectMutation\x121\n" +
|
||||
"\x04type\x18\x01 \x01(\x0e2\x1d.filer_pb.ObjectMutation.TypeR\x04type\x12\x1c\n" +
|
||||
"\tdirectory\x18\x02 \x01(\tR\tdirectory\x12\x12\n" +
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
@@ -27,10 +29,14 @@ func writeConditionSatisfied(cond *filer_pb.WriteCondition, current *filer.Entry
|
||||
|
||||
// clauseSatisfied evaluates one primitive against the current entry. For the
|
||||
// ETag kinds, etags is a set: IF_ETAG_MATCH holds when the current ETag equals
|
||||
// any member, IF_ETAG_NOT_MATCH when it equals none.
|
||||
// any member, IF_ETAG_NOT_MATCH when it equals none. The IF_EXTENDED_* kinds are
|
||||
// generic guards on an extended attribute used to enforce object-lock (legal
|
||||
// hold and retention) without S3 knowledge in the filer.
|
||||
func clauseSatisfied(c *filer_pb.WriteCondition_Clause, current *filer.Entry) bool {
|
||||
exists := current != nil
|
||||
switch c.Kind {
|
||||
case filer_pb.WriteCondition_NONE:
|
||||
return true
|
||||
case filer_pb.WriteCondition_IF_NOT_EXISTS:
|
||||
return !exists
|
||||
case filer_pb.WriteCondition_IF_EXISTS:
|
||||
@@ -43,8 +49,32 @@ func clauseSatisfied(c *filer_pb.WriteCondition_Clause, current *filer.Entry) bo
|
||||
return !exists || current.Attr.Mtime.Unix() <= c.UnixTime
|
||||
case filer_pb.WriteCondition_IF_MODIFIED_SINCE:
|
||||
return !exists || current.Attr.Mtime.Unix() > c.UnixTime
|
||||
case filer_pb.WriteCondition_IF_EXTENDED_NOT_EQUAL:
|
||||
if !exists {
|
||||
return true
|
||||
}
|
||||
v, ok := current.Extended[c.ExtKey]
|
||||
return !ok || string(v) != c.ExtValue
|
||||
case filer_pb.WriteCondition_IF_EXTENDED_TIME_ELAPSED:
|
||||
if !exists {
|
||||
return true
|
||||
}
|
||||
v, ok := current.Extended[c.ExtKey]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
deadline, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64)
|
||||
if err != nil {
|
||||
// An unparseable retention deadline is treated as still in force, so
|
||||
// a malformed attribute fails safe (write blocked) rather than open.
|
||||
return false
|
||||
}
|
||||
return deadline <= time.Now().Unix()
|
||||
default:
|
||||
return true
|
||||
// An unrecognized clause kind (e.g. from a newer client) must not be
|
||||
// treated as satisfied, which would silently bypass the guard. Fail
|
||||
// closed so the write is blocked rather than slipping through.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package weed_server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -105,6 +106,80 @@ func TestWriteConditionClauses(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The generic IF_EXTENDED_* guards express object-lock without S3 knowledge in
|
||||
// the filer: a legal hold (IF_EXTENDED_NOT_EQUAL) and a retention deadline
|
||||
// (IF_EXTENDED_TIME_ELAPSED).
|
||||
func TestWriteConditionObjectLockGuards(t *testing.T) {
|
||||
now := time.Now()
|
||||
withExt := func(ext map[string]string) *filer.Entry {
|
||||
e := &filer.Entry{FullPath: "/test/obj", Attr: filer.Attr{Mtime: now}, Extended: map[string][]byte{}}
|
||||
for k, v := range ext {
|
||||
e.Extended[k] = []byte(v)
|
||||
}
|
||||
return e
|
||||
}
|
||||
legalHold := &filer_pb.WriteCondition{Clauses: []*filer_pb.WriteCondition_Clause{
|
||||
{Kind: filer_pb.WriteCondition_IF_EXTENDED_NOT_EQUAL, ExtKey: "lock-hold", ExtValue: "ON"},
|
||||
}}
|
||||
retention := &filer_pb.WriteCondition{Clauses: []*filer_pb.WriteCondition_Clause{
|
||||
{Kind: filer_pb.WriteCondition_IF_EXTENDED_TIME_ELAPSED, ExtKey: "retain-until"},
|
||||
}}
|
||||
future := strconv.FormatInt(now.Add(time.Hour).Unix(), 10)
|
||||
past := strconv.FormatInt(now.Add(-time.Hour).Unix(), 10)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
cond *filer_pb.WriteCondition
|
||||
cur *filer.Entry
|
||||
want bool
|
||||
}{
|
||||
{"hold-on-blocks", legalHold, withExt(map[string]string{"lock-hold": "ON"}), false},
|
||||
{"hold-off-allows", legalHold, withExt(map[string]string{"lock-hold": "OFF"}), true},
|
||||
{"hold-absent-allows", legalHold, withExt(nil), true},
|
||||
{"hold-on-new-object", legalHold, nil, true}, // nothing to protect yet
|
||||
{"retain-future-blocks", retention, withExt(map[string]string{"retain-until": future}), false},
|
||||
{"retain-past-allows", retention, withExt(map[string]string{"retain-until": past}), true},
|
||||
{"retain-absent-allows", retention, withExt(nil), true},
|
||||
{"retain-malformed-blocks", retention, withExt(map[string]string{"retain-until": "soon"}), false},
|
||||
// Composed WORM guard: legal hold AND retention, both clear -> allowed.
|
||||
{"worm-both-clear", &filer_pb.WriteCondition{Clauses: []*filer_pb.WriteCondition_Clause{
|
||||
{Kind: filer_pb.WriteCondition_IF_EXTENDED_NOT_EQUAL, ExtKey: "lock-hold", ExtValue: "ON"},
|
||||
{Kind: filer_pb.WriteCondition_IF_EXTENDED_TIME_ELAPSED, ExtKey: "retain-until"},
|
||||
}}, withExt(map[string]string{"lock-hold": "OFF", "retain-until": past}), true},
|
||||
// Either guard tripping blocks the whole WORM condition.
|
||||
{"worm-hold-trips", &filer_pb.WriteCondition{Clauses: []*filer_pb.WriteCondition_Clause{
|
||||
{Kind: filer_pb.WriteCondition_IF_EXTENDED_NOT_EQUAL, ExtKey: "lock-hold", ExtValue: "ON"},
|
||||
{Kind: filer_pb.WriteCondition_IF_EXTENDED_TIME_ELAPSED, ExtKey: "retain-until"},
|
||||
}}, withExt(map[string]string{"lock-hold": "ON", "retain-until": past}), false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := writeConditionSatisfied(tc.cond, tc.cur); got != tc.want {
|
||||
t.Errorf("%s: got %v want %v", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An unrecognized clause kind (e.g. from a newer client) fails closed, so a
|
||||
// guard can't be silently bypassed by an older filer. NONE stays a no-op.
|
||||
func TestWriteConditionUnknownKindFailsClosed(t *testing.T) {
|
||||
present := entryWithETag("abc", time.Now())
|
||||
unknown := &filer_pb.WriteCondition{Clauses: []*filer_pb.WriteCondition_Clause{
|
||||
{Kind: filer_pb.WriteCondition_Kind(9999)},
|
||||
}}
|
||||
if writeConditionSatisfied(unknown, present) {
|
||||
t.Error("unknown clause kind must not be satisfied (fail closed) for an existing entry")
|
||||
}
|
||||
if writeConditionSatisfied(unknown, nil) {
|
||||
t.Error("unknown clause kind must not be satisfied (fail closed) for an absent entry")
|
||||
}
|
||||
none := &filer_pb.WriteCondition{Clauses: []*filer_pb.WriteCondition_Clause{
|
||||
{Kind: filer_pb.WriteCondition_NONE},
|
||||
}}
|
||||
if !writeConditionSatisfied(none, present) {
|
||||
t.Error("a NONE clause must be satisfied (no-op)")
|
||||
}
|
||||
}
|
||||
|
||||
// storedEntryETag prefers the stored Seaweed ETag attribute and falls back to
|
||||
// the Md5-derived ETag, matching the S3 gateway.
|
||||
func TestStoredEntryETag(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user