fix(auth): pin native auth storage operations
This commit is contained in:
@@ -9,8 +9,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
@@ -123,21 +121,21 @@ func execute(input request) (response, error) {
|
||||
return response{Version: protocolVersion, OK: true, Prepared: true}, nil
|
||||
}
|
||||
if input.Operation == "read-auth-config" {
|
||||
root, err := existingPrivateRoot(input.Root)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
contents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(root, input.Filename), maximumAuthConfigBytes)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return contentResponse(true, contents), nil
|
||||
return readRootPrivateRegular(input.Root, input.Filename)
|
||||
}
|
||||
if input.Operation == "read-local-users" {
|
||||
return readRootPrivateRegular(input.Root, input.Filename)
|
||||
}
|
||||
if !validDirectory(input.Directory) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
directory, err := storageDirectory(input.Root, input.Directory)
|
||||
if err != nil {
|
||||
layout, err := openStorageLayout(input.Root, true)
|
||||
if err != nil || layout == nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
defer layout.Close()
|
||||
directory := layout.directory(input.Directory)
|
||||
if directory == nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
switch input.Operation {
|
||||
@@ -162,7 +160,7 @@ func execute(input request) (response, error) {
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
if err := safeio.ReplaceCanonicalRegular(filepath.Join(directory, input.Filename), contents, 0o600); err != nil {
|
||||
if err := directory.ReplaceRegular(input.Filename, contents); err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return response{Version: protocolVersion, OK: true, Replaced: true}, nil
|
||||
@@ -177,46 +175,32 @@ func execute(input request) (response, error) {
|
||||
if limit == 0 {
|
||||
limit = defaultMaximumEntries
|
||||
}
|
||||
afterName := ""
|
||||
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
|
||||
afterName = input.AfterName
|
||||
}
|
||||
entries, err := safeio.ListCanonicalPrivateDirectory(directory, limit)
|
||||
page, err := directory.ListPage(limit, afterName, recordListName(input.Directory), recordListLinks(input.Directory))
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !digestFilename.MatchString(entry.Name) && !(input.Directory == "oidc" && (claimFilename.MatchString(entry.Name) || oidcSlotFilename.MatchString(entry.Name))) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
if input.Continuation {
|
||||
more := page.More
|
||||
return response{Version: protocolVersion, OK: true, Entries: &page.Entries, More: &more}, nil
|
||||
}
|
||||
return response{Version: protocolVersion, OK: true, Entries: &entries}, nil
|
||||
if page.More {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return response{Version: protocolVersion, OK: true, Entries: &page.Entries}, nil
|
||||
case "claim-consume":
|
||||
return claimConsume(directory, input.Filename)
|
||||
case "read-claim":
|
||||
contents, found, err := safeio.ReadCanonicalPrivateClaim(
|
||||
filepath.Join(directory, input.Filename),
|
||||
filepath.Join(directory, asClaimFilename(input.Filename)),
|
||||
maximumOIDCStateBytes,
|
||||
)
|
||||
contents, found, err := directory.ReadClaim(input.Filename, asClaimFilename(input.Filename), maximumOIDCStateBytes)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return contentResponse(found, contents), nil
|
||||
case "remove-claim":
|
||||
removed, err := safeio.RemoveCanonicalPrivateClaim(
|
||||
filepath.Join(directory, input.Filename),
|
||||
filepath.Join(directory, asClaimFilename(input.Filename)),
|
||||
)
|
||||
removed, err := directory.RemoveClaim(input.Filename, asClaimFilename(input.Filename))
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
@@ -234,7 +218,7 @@ func validOperationShape(input request) bool {
|
||||
switch input.Operation {
|
||||
case "validate-root", "ensure-layout":
|
||||
return input.Directory == "" && input.Filename == "" && noContents && noMaximumEntries && noAfterName && noContinuation
|
||||
case "read-auth-config":
|
||||
case "read-auth-config", "read-local-users":
|
||||
return input.Directory == "" && authConfigFilename.MatchString(input.Filename) && noContents && noMaximumEntries && noAfterName && noContinuation
|
||||
case "create", "replace":
|
||||
return noMaximumEntries && noAfterName && noContinuation && (digestFilename.MatchString(input.Filename) || (input.Operation == "create" && input.Directory == "oidc" && oidcSlotFilename.MatchString(input.Filename)))
|
||||
@@ -252,78 +236,170 @@ func validOperationShape(input request) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func preflightRoot(root string) (bool, error) {
|
||||
if !filepath.IsAbs(root) || filepath.Clean(root) != root || strings.IndexFunc(root, unicode.IsControl) >= 0 {
|
||||
return false, errInvalid
|
||||
}
|
||||
return safeio.PreflightPrivateDirectory(root)
|
||||
type storageLayout struct {
|
||||
root safeio.PrivateDirectoryHandle
|
||||
sessions safeio.PrivateDirectoryHandle
|
||||
oidc safeio.PrivateDirectoryHandle
|
||||
}
|
||||
|
||||
func existingPrivateRoot(root string) (string, error) {
|
||||
exists, err := preflightRoot(root)
|
||||
if err != nil || !exists || safeio.ValidatePrivateDirectory(root) != nil {
|
||||
return "", errInvalid
|
||||
func (layout *storageLayout) Close() {
|
||||
if layout == nil {
|
||||
return
|
||||
}
|
||||
return root, nil
|
||||
if layout.oidc != nil {
|
||||
_ = layout.oidc.Close()
|
||||
layout.oidc = nil
|
||||
}
|
||||
if layout.sessions != nil {
|
||||
_ = layout.sessions.Close()
|
||||
layout.sessions = nil
|
||||
}
|
||||
if layout.root != nil {
|
||||
_ = layout.root.Close()
|
||||
layout.root = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (layout *storageLayout) closeChildren() {
|
||||
if layout == nil {
|
||||
return
|
||||
}
|
||||
if layout.oidc != nil {
|
||||
_ = layout.oidc.Close()
|
||||
layout.oidc = nil
|
||||
}
|
||||
if layout.sessions != nil {
|
||||
_ = layout.sessions.Close()
|
||||
layout.sessions = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (layout *storageLayout) directory(name string) safeio.PrivateDirectoryHandle {
|
||||
if layout == nil {
|
||||
return nil
|
||||
}
|
||||
if name == "sessions" {
|
||||
return layout.sessions
|
||||
}
|
||||
if name == "oidc" {
|
||||
return layout.oidc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validStorageRoot(root string) bool {
|
||||
return strings.IndexFunc(root, unicode.IsControl) < 0
|
||||
}
|
||||
|
||||
// openStorageLayout keeps the root descriptor/handle open from its initial canonical validation
|
||||
// through every child observation. The side-effect-free path performs a second child pass after
|
||||
// the testable race boundary and retains those final handles; it never validates an earlier,
|
||||
// discarded exists result.
|
||||
func openStorageLayout(root string, ensure bool) (*storageLayout, error) {
|
||||
if !validStorageRoot(root) {
|
||||
return nil, errInvalid
|
||||
}
|
||||
rootHandle, found, err := safeio.OpenPrivateDirectory(root, ensure)
|
||||
if err != nil {
|
||||
return nil, errInvalid
|
||||
}
|
||||
if !found {
|
||||
return nil, nil
|
||||
}
|
||||
layout := &storageLayout{root: rootHandle}
|
||||
failed := true
|
||||
defer func() {
|
||||
if failed {
|
||||
layout.Close()
|
||||
}
|
||||
}()
|
||||
safeio.NotifyPrivateDirectoryTestHookForTest("after-auth-root-open")
|
||||
if err := layout.openChildren(ensure); err != nil {
|
||||
return nil, errInvalid
|
||||
}
|
||||
safeio.NotifyPrivateDirectoryTestHookForTest("after-auth-layout-first-pass")
|
||||
if !ensure {
|
||||
layout.closeChildren()
|
||||
if err := layout.openChildren(false); err != nil {
|
||||
return nil, errInvalid
|
||||
}
|
||||
}
|
||||
if layout.root.Validate() != nil || (layout.sessions != nil && layout.sessions.Validate() != nil) ||
|
||||
(layout.oidc != nil && layout.oidc.Validate() != nil) {
|
||||
return nil, errInvalid
|
||||
}
|
||||
failed = false
|
||||
return layout, nil
|
||||
}
|
||||
|
||||
func (layout *storageLayout) openChildren(ensure bool) error {
|
||||
if layout == nil || layout.root == nil || layout.root.Validate() != nil {
|
||||
return errInvalid
|
||||
}
|
||||
for _, name := range []string{"sessions", "oidc"} {
|
||||
child, found, err := layout.root.OpenChild(name, ensure)
|
||||
if err != nil || (ensure && !found) {
|
||||
if child != nil {
|
||||
_ = child.Close()
|
||||
}
|
||||
return errInvalid
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if child.Validate() != nil {
|
||||
_ = child.Close()
|
||||
return errInvalid
|
||||
}
|
||||
if name == "sessions" {
|
||||
layout.sessions = child
|
||||
} else {
|
||||
layout.oidc = child
|
||||
}
|
||||
}
|
||||
if layout.root.Validate() != nil {
|
||||
return errInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateStorageLayout(root string) error {
|
||||
exists, err := preflightRoot(root)
|
||||
layout, err := openStorageLayout(root, false)
|
||||
if err != nil {
|
||||
return errInvalid
|
||||
}
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
existingChildren := make([]string, 0, 2)
|
||||
for _, directory := range []string{"sessions", "oidc"} {
|
||||
path := filepath.Join(root, directory)
|
||||
if filepath.Dir(path) != root {
|
||||
return errInvalid
|
||||
}
|
||||
childExists, err := safeio.PreflightPrivateDirectory(path)
|
||||
if err != nil {
|
||||
return errInvalid
|
||||
}
|
||||
if childExists {
|
||||
existingChildren = append(existingChildren, path)
|
||||
}
|
||||
}
|
||||
// Close permission/identity races between the individual side-effect-free preflights.
|
||||
if safeio.ValidatePrivateDirectory(root) != nil {
|
||||
return errInvalid
|
||||
}
|
||||
for _, directory := range []string{"sessions", "oidc"} {
|
||||
if _, err := safeio.PreflightPrivateDirectory(filepath.Join(root, directory)); err != nil {
|
||||
return errInvalid
|
||||
}
|
||||
}
|
||||
for _, path := range existingChildren {
|
||||
if safeio.ValidatePrivateDirectory(path) != nil {
|
||||
return errInvalid
|
||||
}
|
||||
if layout != nil {
|
||||
layout.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureStorageLayout(root string) error {
|
||||
if _, err := preflightRoot(root); err != nil || safeio.EnsurePrivateDirectory(root) != nil {
|
||||
return errInvalid
|
||||
}
|
||||
for _, directory := range []string{"sessions", "oidc"} {
|
||||
path := filepath.Join(root, directory)
|
||||
if filepath.Dir(path) != root || safeio.EnsurePrivateDirectory(path) != nil {
|
||||
return errInvalid
|
||||
}
|
||||
}
|
||||
if safeio.ValidatePrivateDirectory(root) != nil ||
|
||||
safeio.ValidatePrivateDirectory(filepath.Join(root, "sessions")) != nil ||
|
||||
safeio.ValidatePrivateDirectory(filepath.Join(root, "oidc")) != nil {
|
||||
layout, err := openStorageLayout(root, true)
|
||||
if err != nil || layout == nil {
|
||||
return errInvalid
|
||||
}
|
||||
layout.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func readRootPrivateRegular(root, filename string) (response, error) {
|
||||
if !validStorageRoot(root) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
directory, found, err := safeio.OpenPrivateDirectory(root, false)
|
||||
if err != nil || !found || directory == nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
defer directory.Close()
|
||||
safeio.NotifyPrivateDirectoryTestHookForTest("after-auth-root-open")
|
||||
contents, found, err := directory.ReadRegular(filename, maximumAuthConfigBytes)
|
||||
if err != nil || !found || directory.Validate() != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return contentResponse(true, contents), nil
|
||||
}
|
||||
|
||||
func contentResponse(found bool, contents []byte) response {
|
||||
if !found {
|
||||
return response{Version: protocolVersion, OK: true}
|
||||
@@ -331,17 +407,6 @@ func contentResponse(found bool, contents []byte) response {
|
||||
return response{Version: protocolVersion, OK: true, Found: true, ContentBase64: base64.StdEncoding.EncodeToString(contents)}
|
||||
}
|
||||
|
||||
func storageDirectory(root, directory string) (string, error) {
|
||||
if _, err := preflightRoot(root); err != nil || safeio.EnsurePrivateDirectory(root) != nil {
|
||||
return "", errInvalid
|
||||
}
|
||||
path := filepath.Join(root, directory)
|
||||
if filepath.Dir(path) != root || safeio.EnsurePrivateDirectory(path) != nil {
|
||||
return "", errInvalid
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func validDirectory(value string) bool {
|
||||
return value == "sessions" || value == "oidc"
|
||||
}
|
||||
@@ -361,67 +426,67 @@ func decodeContents(input request) ([]byte, error) {
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func createPrivate(directory, filename string, contents []byte) (bool, error) {
|
||||
path := filepath.Join(directory, filename)
|
||||
if _, err := os.Lstat(path); err == nil {
|
||||
if safeio.ValidatePrivateRegular(path) != nil {
|
||||
return false, errInvalid
|
||||
}
|
||||
return false, nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
func createPrivate(directory safeio.PrivateDirectoryHandle, filename string, contents []byte) (bool, error) {
|
||||
created, err := directory.CreateRegular(filename, contents)
|
||||
if err != nil {
|
||||
return false, errInvalid
|
||||
}
|
||||
if err := safeio.WriteCanonicalNewPrivateFile(path, contents, 0o600); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if safeio.ValidatePrivateRegular(path) == nil {
|
||||
return false, nil
|
||||
}
|
||||
return false, errInvalid
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func readPrivate(directory, filename string, maximum int64) ([]byte, bool, error) {
|
||||
path := filepath.Join(directory, filename)
|
||||
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
|
||||
return nil, false, nil
|
||||
} else if err != nil {
|
||||
return nil, false, errInvalid
|
||||
}
|
||||
contents, err := safeio.ReadCanonicalPrivateRegular(path, maximum)
|
||||
func readPrivate(directory safeio.PrivateDirectoryHandle, filename string, maximum int64) ([]byte, bool, error) {
|
||||
contents, found, err := directory.ReadRegular(filename, maximum)
|
||||
if err != nil {
|
||||
return nil, false, errInvalid
|
||||
}
|
||||
return contents, true, nil
|
||||
return contents, found, nil
|
||||
}
|
||||
|
||||
func removePrivate(directory, filename string) (bool, error) {
|
||||
path := filepath.Join(directory, filename)
|
||||
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
func removePrivate(directory safeio.PrivateDirectoryHandle, filename string) (bool, error) {
|
||||
removed, err := directory.RemoveRegular(filename)
|
||||
if err != nil {
|
||||
return false, errInvalid
|
||||
}
|
||||
if err := safeio.RemoveCanonicalPrivateRegular(path); err != nil {
|
||||
return false, errInvalid
|
||||
}
|
||||
return true, nil
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
func claimConsume(directory, filename string) (response, error) {
|
||||
source := filepath.Join(directory, filename)
|
||||
claim := filepath.Join(directory, asClaimFilename(filename))
|
||||
claimed, err := safeio.ClaimCanonicalPrivateRegular(source, claim)
|
||||
func recordListName(directory string) func(string) bool {
|
||||
if directory == "sessions" {
|
||||
return digestFilename.MatchString
|
||||
}
|
||||
return func(name string) bool {
|
||||
return digestFilename.MatchString(name) || claimFilename.MatchString(name) || oidcSlotFilename.MatchString(name)
|
||||
}
|
||||
}
|
||||
|
||||
func recordListLinks(directory string) func(string, uint64) bool {
|
||||
if directory == "sessions" {
|
||||
return func(name string, links uint64) bool {
|
||||
return digestFilename.MatchString(name) && links == 1
|
||||
}
|
||||
}
|
||||
return func(name string, links uint64) bool {
|
||||
if digestFilename.MatchString(name) || claimFilename.MatchString(name) {
|
||||
return links == 1 || links == 2
|
||||
}
|
||||
return oidcSlotFilename.MatchString(name) && links == 1
|
||||
}
|
||||
}
|
||||
|
||||
func claimConsume(directory safeio.PrivateDirectoryHandle, filename string) (response, error) {
|
||||
claim := asClaimFilename(filename)
|
||||
claimed, err := directory.ClaimRegular(filename, claim)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
if !claimed {
|
||||
return response{Version: protocolVersion, OK: true}, nil
|
||||
}
|
||||
contents, found, err := safeio.ReadCanonicalPrivateClaim(source, claim, maximumOIDCStateBytes)
|
||||
contents, found, err := directory.ReadClaim(filename, claim, maximumOIDCStateBytes)
|
||||
if err != nil || !found {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
removed, err := safeio.RemoveCanonicalPrivateClaim(source, claim)
|
||||
removed, err := directory.RemoveClaim(filename, claim)
|
||||
if err != nil || !removed {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
|
||||
@@ -167,6 +167,369 @@ func TestProtocolEnsuresTheCompletePrivateSessionLayout(t *testing.T) {
|
||||
runRejected(t, request{Version: 1, Operation: "ensure-layout", Root: root, Directory: "sessions"})
|
||||
}
|
||||
|
||||
func TestProtocolPinsTheOriginalPrivateRootBeforeEveryRecordMutation(t *testing.T) {
|
||||
parent := privateTestRoot(t)
|
||||
root := filepath.Join(parent, "auth")
|
||||
replacement := filepath.Join(parent, "replacement")
|
||||
moved := filepath.Join(parent, "auth-original")
|
||||
filename := "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.json"
|
||||
for _, directory := range []string{root, replacement} {
|
||||
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, child := range []string{"sessions", "oidc"} {
|
||||
if err := safeio.EnsurePrivateDirectory(filepath.Join(directory, child)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swapped := false
|
||||
blocked := false
|
||||
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
|
||||
if stage != "after-auth-root-open" || swapped || blocked {
|
||||
return
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := os.Rename(root, moved); err == nil {
|
||||
t.Fatal("retained Windows root handle permitted rename")
|
||||
}
|
||||
blocked = true
|
||||
return
|
||||
}
|
||||
if err := os.Rename(root, moved); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Rename(replacement, root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
swapped = true
|
||||
})
|
||||
t.Cleanup(restore)
|
||||
|
||||
runRequest(t, request{
|
||||
Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename,
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("pinned-root-record")),
|
||||
})
|
||||
if !swapped && !blocked {
|
||||
t.Fatal("record operation did not expose the retained-root test hook")
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if _, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(root, "sessions", filename), maximumSessionBytes); err != nil {
|
||||
t.Fatalf("record missing from retained Windows root: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
contents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(moved, "sessions", filename), maximumSessionBytes)
|
||||
if err != nil || string(contents) != "pinned-root-record" {
|
||||
t.Fatalf("pinned root contents = %q error = %v", contents, err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(root, "sessions", filename)); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("replacement root was mutated: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolPinsTheOriginalPrivateRootForEveryRecordOperation(t *testing.T) {
|
||||
filename := "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.json"
|
||||
otherFilename := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.json"
|
||||
for _, operation := range []string{
|
||||
"create", "read", "replace", "remove", "list", "claim-consume", "read-claim", "remove-claim",
|
||||
} {
|
||||
t.Run(operation, func(t *testing.T) {
|
||||
parent := privateTestRoot(t)
|
||||
root := filepath.Join(parent, "auth")
|
||||
replacement := filepath.Join(parent, "replacement")
|
||||
moved := filepath.Join(parent, "auth-original")
|
||||
for _, directory := range []string{root, replacement} {
|
||||
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, child := range []string{"sessions", "oidc"} {
|
||||
if err := safeio.EnsurePrivateDirectory(filepath.Join(directory, child)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
write := func(base, directory, name, contents string) {
|
||||
t.Helper()
|
||||
if err := safeio.WriteCanonicalNewPrivateFile(filepath.Join(base, directory, name), []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
pair := func(base, name, contents string) {
|
||||
t.Helper()
|
||||
write(base, "oidc", name, contents)
|
||||
if err := os.Link(filepath.Join(base, "oidc", name), filepath.Join(base, "oidc", asClaimFilename(name))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
input := request{Version: 1, Operation: operation, Root: root}
|
||||
switch operation {
|
||||
case "create":
|
||||
input.Directory, input.Filename = "sessions", filename
|
||||
input.ContentBase64 = base64.StdEncoding.EncodeToString([]byte("original-created"))
|
||||
case "read":
|
||||
write(root, "sessions", filename, "original-read")
|
||||
write(replacement, "sessions", filename, "replacement-read")
|
||||
input.Directory, input.Filename = "sessions", filename
|
||||
case "replace":
|
||||
write(root, "sessions", filename, "original-before")
|
||||
write(replacement, "sessions", filename, "replacement-before")
|
||||
input.Directory, input.Filename = "sessions", filename
|
||||
input.ContentBase64 = base64.StdEncoding.EncodeToString([]byte("original-replaced"))
|
||||
case "remove":
|
||||
write(root, "sessions", filename, "original-remove")
|
||||
write(replacement, "sessions", filename, "replacement-remove")
|
||||
input.Directory, input.Filename = "sessions", filename
|
||||
case "list":
|
||||
write(root, "sessions", filename, "original-list")
|
||||
write(replacement, "sessions", otherFilename, "replacement-list")
|
||||
input.Directory = "sessions"
|
||||
case "claim-consume":
|
||||
write(root, "oidc", filename, "original-claim")
|
||||
write(replacement, "oidc", filename, "replacement-claim")
|
||||
input.Directory, input.Filename = "oidc", filename
|
||||
case "read-claim", "remove-claim":
|
||||
pair(root, filename, "original-pair")
|
||||
pair(replacement, filename, "replacement-pair")
|
||||
input.Directory, input.Filename = "oidc", filename
|
||||
}
|
||||
|
||||
swapped := false
|
||||
blocked := false
|
||||
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
|
||||
if stage != "after-auth-root-open" || swapped || blocked {
|
||||
return
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := os.Rename(root, moved); err == nil {
|
||||
t.Fatal("retained Windows root handle permitted rename")
|
||||
}
|
||||
blocked = true
|
||||
return
|
||||
}
|
||||
if err := os.Rename(root, moved); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Rename(replacement, root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
swapped = true
|
||||
})
|
||||
t.Cleanup(restore)
|
||||
|
||||
output := runRequest(t, input)
|
||||
if !swapped && !blocked {
|
||||
t.Fatal("record operation did not expose the retained-root test hook")
|
||||
}
|
||||
original := root
|
||||
replacementRoot := replacement
|
||||
if swapped {
|
||||
original = moved
|
||||
replacementRoot = root
|
||||
}
|
||||
switch operation {
|
||||
case "create":
|
||||
contents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(original, "sessions", filename), maximumSessionBytes)
|
||||
if err != nil || string(contents) != "original-created" {
|
||||
t.Fatalf("pinned create contents = %q error = %v", contents, err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(replacementRoot, "sessions", filename)); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("replacement root was mutated: %v", err)
|
||||
}
|
||||
case "read":
|
||||
if !output.Found || decodeContent(t, output) != "original-read" {
|
||||
t.Fatalf("pinned read = %#v", output)
|
||||
}
|
||||
case "replace":
|
||||
contents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(original, "sessions", filename), maximumSessionBytes)
|
||||
if err != nil || string(contents) != "original-replaced" {
|
||||
t.Fatalf("pinned replace contents = %q error = %v", contents, err)
|
||||
}
|
||||
replacementContents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(replacementRoot, "sessions", filename), maximumSessionBytes)
|
||||
if err != nil || string(replacementContents) != "replacement-before" {
|
||||
t.Fatalf("replacement contents = %q error = %v", replacementContents, err)
|
||||
}
|
||||
case "remove":
|
||||
if _, err := os.Lstat(filepath.Join(original, "sessions", filename)); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("pinned remove left original record: %v", err)
|
||||
}
|
||||
if _, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(replacementRoot, "sessions", filename), maximumSessionBytes); err != nil {
|
||||
t.Fatalf("replacement record was mutated: %v", err)
|
||||
}
|
||||
case "list":
|
||||
if output.Entries == nil || len(*output.Entries) != 1 || (*output.Entries)[0].Name != filename {
|
||||
t.Fatalf("pinned list = %#v", output)
|
||||
}
|
||||
case "claim-consume":
|
||||
if !output.Found || decodeContent(t, output) != "original-claim" {
|
||||
t.Fatalf("pinned claim consume = %#v", output)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(original, "oidc", filename)); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("pinned claim source remains: %v", err)
|
||||
}
|
||||
if contents, err := safeio.ReadCanonicalPrivateRegular(filepath.Join(replacementRoot, "oidc", filename), maximumOIDCStateBytes); err != nil || string(contents) != "replacement-claim" {
|
||||
t.Fatalf("replacement claim contents = %q error = %v", contents, err)
|
||||
}
|
||||
case "read-claim":
|
||||
if !output.Found || decodeContent(t, output) != "original-pair" {
|
||||
t.Fatalf("pinned claim read = %#v", output)
|
||||
}
|
||||
case "remove-claim":
|
||||
for _, name := range []string{filename, asClaimFilename(filename)} {
|
||||
if _, err := os.Lstat(filepath.Join(original, "oidc", name)); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("pinned claim remove left original %s: %v", name, err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(replacementRoot, "oidc", name)); err != nil {
|
||||
t.Fatalf("replacement claim pair was mutated: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolPinsTheOriginalPrivateRootForLayoutCreation(t *testing.T) {
|
||||
parent := privateTestRoot(t)
|
||||
root := filepath.Join(parent, "auth")
|
||||
replacement := filepath.Join(parent, "replacement")
|
||||
moved := filepath.Join(parent, "auth-original")
|
||||
for _, directory := range []string{root, replacement} {
|
||||
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
swapped := false
|
||||
blocked := false
|
||||
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
|
||||
if stage != "after-auth-root-open" || swapped || blocked {
|
||||
return
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := os.Rename(root, moved); err == nil {
|
||||
t.Fatal("retained Windows root handle permitted rename")
|
||||
}
|
||||
blocked = true
|
||||
return
|
||||
}
|
||||
if err := os.Rename(root, moved); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Rename(replacement, root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
swapped = true
|
||||
})
|
||||
t.Cleanup(restore)
|
||||
|
||||
runRequest(t, request{Version: 1, Operation: "ensure-layout", Root: root})
|
||||
if !swapped && !blocked {
|
||||
t.Fatal("layout creation did not expose the retained-root test hook")
|
||||
}
|
||||
original := root
|
||||
replacementRoot := replacement
|
||||
if swapped {
|
||||
original = moved
|
||||
replacementRoot = root
|
||||
}
|
||||
for _, child := range []string{"sessions", "oidc"} {
|
||||
if err := safeio.ValidatePrivateDirectory(filepath.Join(original, child)); err != nil {
|
||||
t.Fatalf("pinned %s child: %v", child, err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(replacementRoot, child)); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("replacement root received layout mutation: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolValidateRootRechecksChildrenObservedMissingUnderTheRetainedRoot(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
if err := safeio.EnsurePrivateDirectory(root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
installedUnsafeChild := false
|
||||
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
|
||||
if stage != "after-auth-layout-first-pass" || installedUnsafeChild {
|
||||
return
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(root, "sessions"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
installedUnsafeChild = true
|
||||
})
|
||||
t.Cleanup(restore)
|
||||
|
||||
runRejected(t, request{Version: 1, Operation: "validate-root", Root: root})
|
||||
if !installedUnsafeChild {
|
||||
t.Fatal("layout validation did not expose the missing-child recheck hook")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolValidateLayoutPinsTheOriginalPOSIXRootAcrossMissingAndUnsafeChildren(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("native Windows retained-handle coverage uses DACLs in storage_windows_test.go")
|
||||
}
|
||||
for _, scenario := range []struct {
|
||||
name string
|
||||
originalUnsafe bool
|
||||
replacementUnsafe bool
|
||||
wantAccepted bool
|
||||
}{
|
||||
{name: "rejects an unsafe retained child after a safe lexical replacement", originalUnsafe: true, wantAccepted: false},
|
||||
{name: "accepts missing retained children despite an unsafe lexical replacement", replacementUnsafe: true, wantAccepted: true},
|
||||
} {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
parent := privateTestRoot(t)
|
||||
root := filepath.Join(parent, "auth")
|
||||
replacement := filepath.Join(parent, "replacement")
|
||||
moved := filepath.Join(parent, "auth-original")
|
||||
for _, directory := range []string{root, replacement} {
|
||||
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if scenario.originalUnsafe {
|
||||
if err := os.Mkdir(filepath.Join(root, "sessions"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if scenario.replacementUnsafe {
|
||||
if err := os.Mkdir(filepath.Join(replacement, "sessions"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
swapped := false
|
||||
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
|
||||
if stage != "after-auth-root-open" || swapped {
|
||||
return
|
||||
}
|
||||
if err := os.Rename(root, moved); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Rename(replacement, root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
swapped = true
|
||||
})
|
||||
t.Cleanup(restore)
|
||||
input := request{Version: 1, Operation: "validate-root", Root: root}
|
||||
if scenario.wantAccepted {
|
||||
runRequest(t, input)
|
||||
} else {
|
||||
runRejected(t, input)
|
||||
}
|
||||
if !swapped {
|
||||
t.Fatal("layout validation did not retain and expose the original root")
|
||||
}
|
||||
for _, child := range []string{"sessions", "oidc"} {
|
||||
if _, err := os.Lstat(filepath.Join(moved, child)); !errors.Is(err, os.ErrNotExist) && !(scenario.originalUnsafe && child == "sessions") {
|
||||
t.Fatalf("validate-root mutated retained %s child: %v", child, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolReadsOnlyBoundedPrivateAuthConfig(t *testing.T) {
|
||||
root := privateTestRoot(t)
|
||||
filename := "auth.yaml"
|
||||
@@ -199,6 +562,36 @@ func TestProtocolReadsOnlyBoundedPrivateAuthConfig(t *testing.T) {
|
||||
runRejected(t, request{Version: 1, Operation: "read-auth-config", Root: root, Directory: "sessions", Filename: filename})
|
||||
}
|
||||
|
||||
func TestProtocolReadsLocalUsersOnlyAsABoundedPrivateRegularFile(t *testing.T) {
|
||||
root := privateTestRoot(t)
|
||||
filename := "users.yaml"
|
||||
path := filepath.Join(root, filename)
|
||||
contents := []byte("version: 1\nusers: []\n")
|
||||
if err := safeio.WriteCanonicalNewPrivateFile(path, contents, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read := runRequest(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: filename})
|
||||
if !read.Found || decodeContent(t, read) != string(contents) {
|
||||
t.Fatalf("read-local-users = %#v", read)
|
||||
}
|
||||
|
||||
hardLink := filepath.Join(root, "users-copy.yaml")
|
||||
if err := os.Link(path, hardLink); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runRejected(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: filename})
|
||||
if err := os.Remove(hardLink); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Remove(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := safeio.WriteCanonicalNewPrivateFile(path, bytes.Repeat([]byte("x"), maximumAuthConfigBytes+1), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runRejected(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: filename})
|
||||
}
|
||||
|
||||
func TestProtocolPermitsBoundedReservationSlotsOnlyForOIDCRecords(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
slot := "slot-00.json"
|
||||
|
||||
@@ -71,6 +71,96 @@ func TestProtocolValidatesAndCreatesTheCompleteWindowsSessionLayout(t *testing.T
|
||||
runRejected(t, request{Version: 1, Operation: "validate-root", Root: root})
|
||||
}
|
||||
|
||||
func TestProtocolReadsWindowsLocalUsersWithOwnerOnlyDACLAndNoReparseFallback(t *testing.T) {
|
||||
root := filepath.Join(t.TempDir(), "auth")
|
||||
if err := safeio.EnsurePrivateDirectory(root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(root, "users.yaml")
|
||||
contents := []byte("version: 1\nusers: []\n")
|
||||
if err := safeio.WriteCanonicalNewPrivateFile(path, contents, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read := runRequest(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: "users.yaml"})
|
||||
if !read.Found || decodeContent(t, read) != string(contents) {
|
||||
t.Fatalf("read-local-users = %#v", read)
|
||||
}
|
||||
if err := setPermissiveDACL(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runRejected(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: "users.yaml"})
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(filepath.Join(root, "missing-target.yaml"), path); err != nil {
|
||||
t.Skipf("Windows host does not permit test symlink creation: %v", err)
|
||||
}
|
||||
runRejected(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: "users.yaml"})
|
||||
}
|
||||
|
||||
func TestProtocolValidateLayoutPinsWindowsRootAcrossMissingAndUnsafeChildren(t *testing.T) {
|
||||
for _, scenario := range []struct {
|
||||
name string
|
||||
originalUnsafe bool
|
||||
replacementUnsafe bool
|
||||
wantAccepted bool
|
||||
}{
|
||||
{name: "rejects unsafe retained child", originalUnsafe: true, wantAccepted: false},
|
||||
{name: "accepts missing retained child while replacement is unsafe", replacementUnsafe: true, wantAccepted: true},
|
||||
} {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
parent := t.TempDir()
|
||||
root := filepath.Join(parent, "auth")
|
||||
replacement := filepath.Join(parent, "replacement")
|
||||
moved := filepath.Join(parent, "auth-original")
|
||||
for _, directory := range []string{root, replacement} {
|
||||
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if scenario.originalUnsafe {
|
||||
path := filepath.Join(root, "sessions")
|
||||
if err := safeio.EnsurePrivateDirectory(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := setPermissiveDACL(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if scenario.replacementUnsafe {
|
||||
path := filepath.Join(replacement, "sessions")
|
||||
if err := safeio.EnsurePrivateDirectory(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := setPermissiveDACL(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
blocked := false
|
||||
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
|
||||
if stage != "after-auth-root-open" || blocked {
|
||||
return
|
||||
}
|
||||
if err := os.Rename(root, moved); err == nil {
|
||||
t.Fatal("retained Windows root handle permitted rename")
|
||||
}
|
||||
blocked = true
|
||||
})
|
||||
t.Cleanup(restore)
|
||||
input := request{Version: 1, Operation: "validate-root", Root: root}
|
||||
if scenario.wantAccepted {
|
||||
runRequest(t, input)
|
||||
} else {
|
||||
runRejected(t, input)
|
||||
}
|
||||
if !blocked {
|
||||
t.Fatal("layout validation did not retain the Windows root handle")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setPermissiveDACL(path string) error {
|
||||
world, err := windows.StringToSid("S-1-1-0")
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user