fix(auth): paginate session maintenance safely
This commit is contained in:
@@ -41,6 +41,8 @@ type request struct {
|
||||
Filename string `json:"filename,omitempty"`
|
||||
ContentBase64 string `json:"contentBase64,omitempty"`
|
||||
MaximumEntries int `json:"maximumEntries,omitempty"`
|
||||
AfterName string `json:"afterName,omitempty"`
|
||||
Continuation bool `json:"continuation,omitempty"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
@@ -53,6 +55,7 @@ type response struct {
|
||||
Claimed bool `json:"claimed,omitempty"`
|
||||
ContentBase64 string `json:"contentBase64,omitempty"`
|
||||
Entries *[]safeio.PrivateDirectoryEntry `json:"entries,omitempty"`
|
||||
More *bool `json:"more,omitempty"`
|
||||
}
|
||||
|
||||
// Run accepts exactly one strict JSON request on stdin and emits exactly one JSON response on
|
||||
@@ -143,6 +146,19 @@ func execute(input request) (response, error) {
|
||||
if limit == 0 {
|
||||
limit = defaultMaximumEntries
|
||||
}
|
||||
if input.Continuation {
|
||||
page, err := safeio.ListCanonicalPrivateDirectoryPage(
|
||||
directory,
|
||||
limit,
|
||||
input.AfterName,
|
||||
digestFilename.MatchString,
|
||||
)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
more := page.More
|
||||
return response{Version: protocolVersion, OK: true, Entries: &page.Entries, More: &more}, nil
|
||||
}
|
||||
entries, err := safeio.ListCanonicalPrivateDirectory(directory, limit)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
@@ -182,17 +198,20 @@ func execute(input request) (response, error) {
|
||||
func validOperationShape(input request) bool {
|
||||
noContents := input.ContentBase64 == ""
|
||||
noMaximumEntries := input.MaximumEntries == 0
|
||||
noAfterName := input.AfterName == ""
|
||||
noContinuation := !input.Continuation
|
||||
switch input.Operation {
|
||||
case "create", "replace":
|
||||
return noMaximumEntries && (digestFilename.MatchString(input.Filename) || (input.Operation == "create" && input.Directory == "oidc" && oidcSlotFilename.MatchString(input.Filename)))
|
||||
return noMaximumEntries && noAfterName && noContinuation && (digestFilename.MatchString(input.Filename) || (input.Operation == "create" && input.Directory == "oidc" && oidcSlotFilename.MatchString(input.Filename)))
|
||||
case "read":
|
||||
return noContents && noMaximumEntries && (digestFilename.MatchString(input.Filename) || (input.Directory == "oidc" && oidcSlotFilename.MatchString(input.Filename)))
|
||||
return noContents && noMaximumEntries && noAfterName && noContinuation && (digestFilename.MatchString(input.Filename) || (input.Directory == "oidc" && oidcSlotFilename.MatchString(input.Filename)))
|
||||
case "remove":
|
||||
return noContents && noMaximumEntries && (digestFilename.MatchString(input.Filename) || (input.Directory == "oidc" && (claimFilename.MatchString(input.Filename) || oidcSlotFilename.MatchString(input.Filename))))
|
||||
return noContents && noMaximumEntries && noAfterName && noContinuation && (digestFilename.MatchString(input.Filename) || (input.Directory == "oidc" && (claimFilename.MatchString(input.Filename) || oidcSlotFilename.MatchString(input.Filename))))
|
||||
case "list":
|
||||
return input.Filename == "" && noContents && input.MaximumEntries >= 0 && input.MaximumEntries <= maximumEntries
|
||||
return input.Filename == "" && noContents && input.MaximumEntries >= 0 && input.MaximumEntries <= maximumEntries &&
|
||||
((noContinuation && noAfterName) || (input.Continuation && input.Directory == "sessions" && input.MaximumEntries >= 1 && (noAfterName || digestFilename.MatchString(input.AfterName))))
|
||||
case "claim-consume", "read-claim", "remove-claim":
|
||||
return input.Directory == "oidc" && noContents && noMaximumEntries && digestFilename.MatchString(input.Filename)
|
||||
return input.Directory == "oidc" && noContents && noMaximumEntries && noAfterName && noContinuation && digestFilename.MatchString(input.Filename)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -34,6 +34,34 @@ func TestProtocolListUsesACallerSuppliedBound(t *testing.T) {
|
||||
runRejected(t, request{Version: 1, Operation: "read", Root: root, Directory: "sessions", Filename: filename, MaximumEntries: 1})
|
||||
}
|
||||
|
||||
func TestProtocolListPaginatesOrdinarySessionRecords(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
for index := 0; index < 513; index++ {
|
||||
filename := fmt.Sprintf("%064x.json", index)
|
||||
runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("record"))})
|
||||
}
|
||||
|
||||
first := runRequest(t, request{Version: 1, Operation: "list", Root: root, Directory: "sessions", MaximumEntries: 512, Continuation: true})
|
||||
if first.Entries == nil || len(*first.Entries) != 512 || first.More == nil || !*first.More {
|
||||
t.Fatalf("first continuation page = %#v", first)
|
||||
}
|
||||
second := runRequest(t, request{
|
||||
Version: 1,
|
||||
Operation: "list",
|
||||
Root: root,
|
||||
Directory: "sessions",
|
||||
MaximumEntries: 512,
|
||||
Continuation: true,
|
||||
AfterName: (*first.Entries)[511].Name,
|
||||
})
|
||||
if second.Entries == nil || len(*second.Entries) != 1 || second.More == nil || *second.More || (*second.Entries)[0].Name != fmt.Sprintf("%064x.json", 512) {
|
||||
t.Fatalf("second continuation page = %#v", second)
|
||||
}
|
||||
|
||||
runRejected(t, request{Version: 1, Operation: "list", Root: root, Directory: "oidc", MaximumEntries: 1, Continuation: true, AfterName: fmt.Sprintf("%064x.json", 0)})
|
||||
runRejected(t, request{Version: 1, Operation: "list", Root: root, Directory: "sessions", MaximumEntries: 1, Continuation: true, AfterName: "../unsafe.json"})
|
||||
}
|
||||
|
||||
func TestProtocolCreatesReadsReplacesListsAndRemovesPrivateRecord(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
filename := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.json"
|
||||
@@ -215,6 +243,7 @@ func TestProtocolRejectsBoundsReparseAndUnexpectedStorageNames(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runRejected(t, request{Version: 1, Operation: "list", Root: root, Directory: "sessions"})
|
||||
runRejected(t, request{Version: 1, Operation: "list", Root: root, Directory: "sessions", MaximumEntries: 1, Continuation: true})
|
||||
|
||||
outer := privateTestRoot(t)
|
||||
linkedRoot := filepath.Join(outer, "linked-auth")
|
||||
|
||||
Reference in New Issue
Block a user