Hopefully fix some bugs.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package authproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -14,6 +17,13 @@ type accessLists struct {
|
||||
AllowedDomains []string
|
||||
AllowedEmails []string
|
||||
DeniedEmails []string
|
||||
|
||||
// Unreadable records that one of the app's list files exists but could not
|
||||
// be read, leaving its real rules unknown. Falling back to the global list
|
||||
// would then silently widen access — the app is likely narrower than global,
|
||||
// which is why it has a list at all — so an app in this state denies
|
||||
// everyone until the file is readable again.
|
||||
Unreadable bool
|
||||
}
|
||||
|
||||
func (a accessLists) hasAllowRules() bool {
|
||||
@@ -64,26 +74,40 @@ func (s *appStore) lists(app string) accessLists {
|
||||
return hit.lists
|
||||
}
|
||||
|
||||
lists := accessLists{
|
||||
AllowedDomains: readDomainFile(filepath.Join(s.dir, app, "allowed-domains")),
|
||||
AllowedEmails: readListFile(filepath.Join(s.dir, app, "allowed-emails")),
|
||||
DeniedEmails: readListFile(filepath.Join(s.dir, app, "denied-emails")),
|
||||
var lists accessLists
|
||||
read := func(name string) []string {
|
||||
entries, err := readListFile(filepath.Join(s.dir, app, name))
|
||||
if err != nil {
|
||||
log.Printf("appconfig: cannot read %s for app %q: %v — denying every account "+
|
||||
"for that app until it is readable", name, app, err)
|
||||
lists.Unreadable = true
|
||||
}
|
||||
return entries
|
||||
}
|
||||
lists.AllowedDomains = trimAts(read("allowed-domains"))
|
||||
lists.AllowedEmails = read("allowed-emails")
|
||||
lists.DeniedEmails = read("denied-emails")
|
||||
|
||||
s.cache[app] = cachedLists{lists: lists, loadedAt: s.now()}
|
||||
return lists
|
||||
}
|
||||
|
||||
func readListFile(path string) []string {
|
||||
// A missing or unreadable file is normal: most apps have no overrides.
|
||||
// readListFile returns the entries in a list file. A missing file is normal —
|
||||
// most apps have no overrides — and reads as an empty list; any other error is
|
||||
// reported, because it means the app's rules are unknown rather than absent.
|
||||
func readListFile(path string) ([]string, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return splitList(string(raw))
|
||||
return splitList(string(raw)), nil
|
||||
}
|
||||
|
||||
func readDomainFile(path string) []string {
|
||||
entries := readListFile(path)
|
||||
// trimAts accepts domains written either as "signal.org" or "@signal.org".
|
||||
func trimAts(entries []string) []string {
|
||||
for i, d := range entries {
|
||||
entries[i] = strings.TrimPrefix(d, "@")
|
||||
}
|
||||
|
||||
@@ -53,6 +53,37 @@ func TestAppStoreReadsLists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A list file the service cannot read leaves the app's real rules unknown.
|
||||
// Inheriting the global list there would hand out access the app's own list was
|
||||
// written to withhold, so the app denies instead.
|
||||
func TestUnreadableListDeniesInsteadOfInheriting(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("root reads files regardless of mode")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
writeAppLists(t, dir, "locked", map[string]string{"allowed-emails": "ceo@signal.org\n"})
|
||||
if err := os.Chmod(filepath.Join(dir, "locked", "allowed-emails"), 0o000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := newTestServer(t, "http://unused.invalid") // global list allows all of signal.org
|
||||
s.cfg.AppConfigDir = dir
|
||||
s.apps = newAppStore(dir)
|
||||
|
||||
if !s.apps.lists("locked").Unreadable {
|
||||
t.Error("an unreadable list file should mark the app's lists unreadable")
|
||||
}
|
||||
for _, email := range []string{"ceo@signal.org", "anyone@signal.org"} {
|
||||
if s.emailAllowedFor("locked", email) {
|
||||
t.Errorf("emailAllowedFor(locked, %q) = true; an app whose list cannot be read must deny", email)
|
||||
}
|
||||
}
|
||||
// Only that app is affected: everyone else still uses the global list.
|
||||
if !s.emailAllowedFor("inherits", "anyone@signal.org") {
|
||||
t.Error("an app with no config of its own should still inherit the global list")
|
||||
}
|
||||
}
|
||||
|
||||
// The app name reaches the service in a header, so it must never be able to
|
||||
// walk out of the config directory.
|
||||
func TestAppStoreRejectsUnusableNames(t *testing.T) {
|
||||
|
||||
@@ -313,6 +313,7 @@ func (s *Server) effectiveLists(app string) accessLists {
|
||||
AllowedDomains: s.cfg.AllowedDomains,
|
||||
AllowedEmails: s.cfg.AllowedEmails,
|
||||
DeniedEmails: s.cfg.DeniedEmails,
|
||||
Unreadable: own.Unreadable,
|
||||
}
|
||||
if own.hasAllowRules() {
|
||||
out.AllowedDomains = own.AllowedDomains
|
||||
@@ -326,10 +327,16 @@ func (s *Server) effectiveLists(app string) accessLists {
|
||||
|
||||
// emailAllowedFor applies app's rules: the deny list first (it always wins),
|
||||
// then the allow list, where an address must match an allowed email or an
|
||||
// allowed domain. An empty app means "global rules only".
|
||||
// allowed domain. An empty app means "global rules only"; an app whose own
|
||||
// lists could not be read is denied outright rather than quietly handed the
|
||||
// broader global list.
|
||||
func (s *Server) emailAllowedFor(app, email string) bool {
|
||||
email = strings.ToLower(email)
|
||||
lists := s.effectiveLists(app)
|
||||
if lists.Unreadable {
|
||||
// The app has rules we could not read; see accessLists.Unreadable.
|
||||
return false
|
||||
}
|
||||
if slices.Contains(lists.DeniedEmails, email) {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user