updated styling and tests (#1116)
This commit is contained in:
1 parent
8a597e3202
commit
70933aea36
21 files changed
+508
-106
No files matched your search
+6
-3
@@ -4,15 +4,18 @@ All notable changes to this project will be documented in this file. For commit
|
||||
|
||||
## v0.8.2-beta
|
||||
|
||||
Includes new 8.0 ffmpeg version bundled with docker and go 1.25 upgrade with new GC flag enabled. I found this new garbage collector reduced memory usage by an astounding 38% on my personal deployment. Indexing performance also appears 25% faster. I have opened [this poll](https://github.com/gtsteffaniak/filebrowser/discussions/1113) to get your feedback on the change.
|
||||
**New Features**:
|
||||
- added `source.config.denyByDefault` configuration to enable a deny-by-default access rule. A source enabled with this will deny access unless an "allow" rule was specifically created. (Similar to creating a root-level denyAll rule)
|
||||
|
||||
**Notes**:
|
||||
- updated description for indexingIntervalMinutes https://github.com/gtsteffaniak/filebrowser/issues/1067
|
||||
- 8.0 ffmpeg version bundled with docker
|
||||
- go 1.25 upgrade with green tea GC enabled
|
||||
|
||||
**BugFixes**:
|
||||
- fixed routes issue
|
||||
- fixed /public/static routes issue
|
||||
- shares redirect to login - https://github.com/gtsteffaniak/filebrowser/issues/1109
|
||||
- some static assets not available to anonymous user - https://github.com/gtsteffaniak/filebrowser/issues/1102
|
||||
- more safari style issues https://github.com/gtsteffaniak/filebrowser/issues/1110
|
||||
|
||||
## v0.8.1-beta
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ type Source struct {
|
||||
}
|
||||
|
||||
type SourceConfig struct {
|
||||
DenyByDefault bool `json:"denyByDefault"` // deny access unless an "allow" access rule was specifically created.
|
||||
Private bool `json:"private"` // designate as source as private -- currently just means no sharing permitted.
|
||||
Disabled bool `json:"disabled"` // disable the source, this is useful so you don't need to remove it from the config file
|
||||
IndexingInterval uint32 `json:"indexingIntervalMinutes"` // optional manual overide interval in minutes to re-index the source
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
"github.com/gtsteffaniak/go-cache/cache"
|
||||
@@ -52,9 +53,10 @@ type FrontendRuleSet struct {
|
||||
}
|
||||
|
||||
type FrontendAccessRule struct {
|
||||
DenyAll bool `json:"denyAll,omitempty"`
|
||||
Deny FrontendRuleSet `json:"deny"`
|
||||
Allow FrontendRuleSet `json:"allow"`
|
||||
DenyAll bool `json:"denyAll,omitempty"`
|
||||
Deny FrontendRuleSet `json:"deny"`
|
||||
Allow FrontendRuleSet `json:"allow"`
|
||||
SourceDenyDefault bool `json:"sourceDenyDefault"`
|
||||
}
|
||||
|
||||
// GroupMap maps group names to a set of usernames.
|
||||
@@ -134,6 +136,12 @@ func NewStorage(db *storm.DB, usersStore *users.Storage) *Storage {
|
||||
return s
|
||||
}
|
||||
|
||||
// ClearCache clears the access cache (useful for testing)
|
||||
func ClearCache() {
|
||||
// Recreate the cache to clear it
|
||||
accessCache = cache.NewCache(1 * time.Minute)
|
||||
}
|
||||
|
||||
// getOrCreateRuleNL ensures a rule exists for the given source and index path.
|
||||
// The caller must hold the lock.
|
||||
func (s *Storage) getOrCreateRuleNL(sourcePath, indexPath string) *AccessRule {
|
||||
@@ -239,6 +247,7 @@ func (s *Storage) DenyAll(sourcePath, indexPath string) error {
|
||||
|
||||
// Permitted checks if a username is permitted for a given sourcePath and indexPath, recursively checking parent directories.
|
||||
func (s *Storage) Permitted(sourcePath, indexPath, username string) bool {
|
||||
|
||||
// Get current version for the sourcePath
|
||||
versionKey := "version:" + sourcePath
|
||||
version := 0
|
||||
@@ -269,6 +278,19 @@ func (s *Storage) computePermitted(sourcePath, indexPath, username string) bool
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// No rules found anywhere in the path hierarchy
|
||||
// Check if the source has DenyByDefault configured (acts like a root-level denyAll rule)
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[sourcePath]
|
||||
if !ok {
|
||||
logger.Errorf("source %s not found in config during access check", sourcePath)
|
||||
return false
|
||||
}
|
||||
|
||||
if sourceInfo.Config.DenyByDefault {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -285,28 +307,27 @@ func (s *Storage) permittedAtExactPath(sourcePath, indexPath, username string) (
|
||||
if !ok {
|
||||
return true, false
|
||||
}
|
||||
return s.permittedRule(rule, username), true
|
||||
return s.permittedRule(rule, username, sourcePath), true
|
||||
}
|
||||
|
||||
// permittedRule contains the old Permitted logic, but operates on a rule and username only.
|
||||
func (s *Storage) permittedRule(rule *AccessRule, username string) bool {
|
||||
// Check user deny
|
||||
func (s *Storage) permittedRule(rule *AccessRule, username string, sourcePath string) bool {
|
||||
// Check user deny first - this always takes precedence
|
||||
if _, found := rule.Deny.Users[username]; found {
|
||||
return false
|
||||
}
|
||||
if rule.DenyAll {
|
||||
return false
|
||||
}
|
||||
// Check group deny
|
||||
for group := range rule.Deny.Groups {
|
||||
if s.isUserInGroup(username, group) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// If any allow is present, user must be in at least one
|
||||
|
||||
// Check allow rules - these can override DenyAll
|
||||
hasUserAllow := len(rule.Allow.Users) > 0
|
||||
hasGroupAllow := len(rule.Allow.Groups) > 0
|
||||
if hasUserAllow || hasGroupAllow {
|
||||
// If user is in any allow list, permit them (overrides DenyAll)
|
||||
if hasUserAllow {
|
||||
if _, found := rule.Allow.Users[username]; found {
|
||||
return true
|
||||
@@ -319,18 +340,30 @@ func (s *Storage) permittedRule(rule *AccessRule, username string) bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
// Fallback to deny rules
|
||||
if _, found := rule.Deny.Users[username]; found {
|
||||
return false
|
||||
}
|
||||
for group := range rule.Deny.Groups {
|
||||
if s.isUserInGroup(username, group) {
|
||||
|
||||
// User is not in any allow list
|
||||
// Check DenyByDefault to determine behavior
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[sourcePath]
|
||||
if ok && sourceInfo.Config.DenyByDefault {
|
||||
// DenyByDefault=true: allow lists are exclusive, deny users not on the list
|
||||
return false
|
||||
} else {
|
||||
// DenyByDefault=false: allow lists are additive, allow users not on the list
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check DenyAll - only applies if no allow rules exist
|
||||
if rule.DenyAll {
|
||||
return false
|
||||
}
|
||||
|
||||
// No specific rules apply - check source DenyByDefault setting
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[sourcePath]
|
||||
if ok && sourceInfo.Config.DenyByDefault {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -350,7 +383,16 @@ func (s *Storage) isUserInGroup(username, group string) bool {
|
||||
func (s *Storage) GetFrontendRules(sourcePath, indexPath string) (FrontendAccessRule, bool) {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
frontendRules := FrontendAccessRule{
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: make([]string, 0),
|
||||
Groups: make([]string, 0),
|
||||
@@ -388,6 +430,14 @@ func (s *Storage) GetAllRules(sourcePath string) (map[string]FrontendAccessRule,
|
||||
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
// Return a copy to avoid external mutation
|
||||
frontendRules := make(map[string]FrontendAccessRule, len(s.AllRules))
|
||||
rules, ok := s.AllRules[sourcePath]
|
||||
@@ -397,7 +447,8 @@ func (s *Storage) GetAllRules(sourcePath string) (map[string]FrontendAccessRule,
|
||||
for indexPath, rule := range rules {
|
||||
// Convert AccessRule to FrontendAccessRule
|
||||
frontendRules[indexPath] = FrontendAccessRule{
|
||||
DenyAll: rule.DenyAll,
|
||||
DenyAll: rule.DenyAll,
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Users))),
|
||||
Groups: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups))),
|
||||
@@ -739,6 +790,14 @@ func (s *Storage) RemoveAllRulesForGroup(groupname string) error {
|
||||
func (s *Storage) GetRulesForUser(sourcePath, username string) map[string]FrontendAccessRule {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
userRules := make(map[string]FrontendAccessRule)
|
||||
rulesBySource, ok := s.AllRules[sourcePath]
|
||||
if !ok {
|
||||
@@ -756,7 +815,8 @@ func (s *Storage) GetRulesForUser(sourcePath, username string) map[string]Fronte
|
||||
}
|
||||
if userHasRule {
|
||||
userRules[indexPath] = FrontendAccessRule{
|
||||
DenyAll: rule.DenyAll,
|
||||
DenyAll: rule.DenyAll,
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Users))),
|
||||
Groups: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups))),
|
||||
@@ -775,6 +835,14 @@ func (s *Storage) GetRulesForUser(sourcePath, username string) map[string]Fronte
|
||||
func (s *Storage) GetRulesForGroup(sourcePath, groupname string) map[string]FrontendAccessRule {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
groupRules := make(map[string]FrontendAccessRule)
|
||||
rulesBySource, ok := s.AllRules[sourcePath]
|
||||
if !ok {
|
||||
@@ -792,7 +860,8 @@ func (s *Storage) GetRulesForGroup(sourcePath, groupname string) map[string]Fron
|
||||
}
|
||||
if groupHasRule {
|
||||
groupRules[indexPath] = FrontendAccessRule{
|
||||
DenyAll: rule.DenyAll,
|
||||
DenyAll: rule.DenyAll,
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Users))),
|
||||
Groups: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups))),
|
||||
@@ -811,6 +880,14 @@ func (s *Storage) GetRulesForGroup(sourcePath, groupname string) map[string]Fron
|
||||
func (s *Storage) GetAllRulesByUsers(sourcePath string) map[string]map[string]FrontendAccessRule {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
allUserRules := make(map[string]map[string]FrontendAccessRule)
|
||||
rulesBySource, ok := s.AllRules[sourcePath]
|
||||
if !ok {
|
||||
@@ -823,7 +900,8 @@ func (s *Storage) GetAllRulesByUsers(sourcePath string) map[string]map[string]Fr
|
||||
continue
|
||||
}
|
||||
frontendRule := FrontendAccessRule{
|
||||
DenyAll: rule.DenyAll,
|
||||
DenyAll: rule.DenyAll,
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Users))),
|
||||
Groups: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups))),
|
||||
@@ -853,6 +931,14 @@ func (s *Storage) GetAllRulesByUsers(sourcePath string) map[string]map[string]Fr
|
||||
func (s *Storage) GetAllRulesByGroups(sourcePath string) map[string]map[string]FrontendAccessRule {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
allGroupRules := make(map[string]map[string]FrontendAccessRule)
|
||||
rulesBySource, ok := s.AllRules[sourcePath]
|
||||
if !ok {
|
||||
@@ -865,7 +951,8 @@ func (s *Storage) GetAllRulesByGroups(sourcePath string) map[string]map[string]F
|
||||
continue
|
||||
}
|
||||
frontendRule := FrontendAccessRule{
|
||||
DenyAll: rule.DenyAll,
|
||||
DenyAll: rule.DenyAll,
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Users))),
|
||||
Groups: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups))),
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/access"
|
||||
boltusers "github.com/gtsteffaniak/filebrowser/backend/database/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
@@ -29,7 +30,28 @@ func createTestUser(t *testing.T, userStore *users.Storage, username string) {
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestSources() {
|
||||
// Setup default test sources with allow-by-default behavior
|
||||
settings.Config.Server.SourceMap = map[string]settings.Source{
|
||||
"mnt/storage": {
|
||||
Path: "mnt/storage",
|
||||
Name: "storage",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: false,
|
||||
},
|
||||
},
|
||||
"mnt/open": {
|
||||
Path: "mnt/open",
|
||||
Name: "open",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_UserBlacklist(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -49,6 +71,7 @@ func TestPermitted_UserBlacklist(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPermitted_UserWhitelist(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -63,11 +86,12 @@ func TestPermitted_UserWhitelist(t *testing.T) {
|
||||
t.Error("bob should be permitted (allowed)")
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/vip", "alice") {
|
||||
t.Error("alice should be permitted (not denied)")
|
||||
t.Error("alice should be permitted (default allow behavior when DenyByDefault=false)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_GroupBlacklist(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -88,6 +112,7 @@ func TestPermitted_GroupBlacklist(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPermitted_GroupWhitelist(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -103,11 +128,12 @@ func TestPermitted_GroupWhitelist(t *testing.T) {
|
||||
t.Error("bob should be permitted (in allowed group)")
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/vip", "alice") {
|
||||
t.Error("alice should be permitted (not in denied group)")
|
||||
t.Error("alice should be permitted (default allow behavior when DenyByDefault=false)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_NoRule(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, _ := createTestStorage(t)
|
||||
err := s.LoadFromDB()
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
@@ -119,6 +145,7 @@ func TestPermitted_NoRule(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPermitted_CombinedRules(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -163,6 +190,7 @@ func TestPermitted_CombinedRules(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPermitted_DenyAll(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -182,12 +210,12 @@ func TestPermitted_DenyAll(t *testing.T) {
|
||||
t.Error("bob should not be permitted (deny all)")
|
||||
}
|
||||
|
||||
// Test that Allow rule is overridden by DenyAll
|
||||
// Test that Allow rule overrides DenyAll
|
||||
if err = s.AllowUser("mnt/storage", "/private", "alice"); err != nil {
|
||||
t.Errorf("AllowUser failed: %v", err)
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/private", "alice") {
|
||||
t.Error("alice should not be permitted (deny all overrides allow)")
|
||||
if !s.Permitted("mnt/storage", "/private", "alice") {
|
||||
t.Error("alice should be permitted (allow overrides deny all)")
|
||||
}
|
||||
|
||||
// Test removing DenyAll
|
||||
@@ -204,8 +232,176 @@ func TestPermitted_DenyAll(t *testing.T) {
|
||||
t.Error("alice should be permitted after removing deny all")
|
||||
}
|
||||
|
||||
// Bob should be permitted as he is not on any list, and there is an allow list which he is not on, but he is not on deny either.
|
||||
// Bob should be permitted because DenyByDefault=false makes allow lists additive (not exclusive)
|
||||
if !s.Permitted("mnt/storage", "/private", "bob") {
|
||||
t.Error("bob should be permitted after removing deny all (not on allow list but not denied)")
|
||||
t.Error("bob should be permitted after removing deny all (DenyByDefault=false makes allow lists additive)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_DenyByDefault(t *testing.T) {
|
||||
// Clear access cache to prevent test pollution
|
||||
access.ClearCache()
|
||||
|
||||
// Create isolated storage for this test
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
err := s.LoadFromDB()
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
t.Errorf("unexpected error loading from DB: %v", err)
|
||||
}
|
||||
|
||||
// Setup test configuration with DenyByDefault enabled for one source
|
||||
originalSourceMap := settings.Config.Server.SourceMap
|
||||
defer func() {
|
||||
settings.Config.Server.SourceMap = originalSourceMap
|
||||
access.ClearCache()
|
||||
}()
|
||||
|
||||
settings.Config.Server.SourceMap = map[string]settings.Source{
|
||||
"mnt/storage": {
|
||||
Path: "mnt/storage",
|
||||
Name: "storage",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: true,
|
||||
},
|
||||
},
|
||||
"mnt/open": {
|
||||
Path: "mnt/open",
|
||||
Name: "open",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test DenyByDefault: When no rules exist, users should be denied
|
||||
if s.Permitted("mnt/storage", "/private", "alice") {
|
||||
t.Error("alice should not be permitted (DenyByDefault is true and no rules exist)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/private", "bob") {
|
||||
t.Error("bob should not be permitted (DenyByDefault is true and no rules exist)")
|
||||
}
|
||||
|
||||
// Test that explicit allow rules override DenyByDefault
|
||||
if err := s.AllowUser("mnt/storage", "/allowed", "alice"); err != nil {
|
||||
t.Errorf("AllowUser failed: %v", err)
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/allowed", "alice") {
|
||||
t.Error("alice should be permitted (explicit allow rule overrides DenyByDefault)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/allowed", "bob") {
|
||||
t.Error("bob should not be permitted (not in allow list and DenyByDefault is true)")
|
||||
}
|
||||
|
||||
// Test that explicit deny rules work with DenyByDefault
|
||||
if err := s.DenyUser("mnt/storage", "/denied", "alice"); err != nil {
|
||||
t.Errorf("DenyUser failed: %v", err)
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/denied", "alice") {
|
||||
t.Error("alice should not be permitted (explicit deny rule)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/denied", "bob") {
|
||||
t.Error("bob should not be permitted (DenyByDefault is true)")
|
||||
}
|
||||
|
||||
// Test that DenyByDefault doesn't affect sources where it's disabled
|
||||
if !s.Permitted("mnt/open", "/public", "alice") {
|
||||
t.Error("alice should be permitted in open source (DenyByDefault is false)")
|
||||
}
|
||||
if !s.Permitted("mnt/open", "/public", "bob") {
|
||||
t.Error("bob should be permitted in open source (DenyByDefault is false)")
|
||||
}
|
||||
|
||||
// Test recursive path checking with DenyByDefault
|
||||
// Allow alice at parent path, should work for child paths too
|
||||
if err := s.AllowUser("mnt/storage", "/parent", "alice"); err != nil {
|
||||
t.Errorf("AllowUser failed: %v", err)
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/parent/child", "alice") {
|
||||
t.Error("alice should be permitted for child path (inherits from parent allow rule)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/parent/child", "bob") {
|
||||
t.Error("bob should not be permitted for child path (DenyByDefault and not in allow list)")
|
||||
}
|
||||
|
||||
// Test that group rules work with DenyByDefault
|
||||
_ = s.AddUserToGroup("vip", "alice")
|
||||
if err := s.AllowGroup("mnt/storage", "/vip", "vip"); err != nil {
|
||||
t.Errorf("AllowGroup failed: %v", err)
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/vip", "alice") {
|
||||
t.Error("alice should be permitted (in allowed group)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/vip", "bob") {
|
||||
t.Error("bob should not be permitted (not in allowed group and DenyByDefault is true)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_DenyByDefaultWithDenyAll(t *testing.T) {
|
||||
// Clear access cache to prevent test pollution
|
||||
access.ClearCache()
|
||||
|
||||
// Create isolated storage for this test
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
err := s.LoadFromDB()
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
t.Errorf("unexpected error loading from DB: %v", err)
|
||||
}
|
||||
|
||||
// Setup test configuration with DenyByDefault enabled
|
||||
originalSourceMap := settings.Config.Server.SourceMap
|
||||
defer func() {
|
||||
settings.Config.Server.SourceMap = originalSourceMap
|
||||
access.ClearCache()
|
||||
}()
|
||||
|
||||
settings.Config.Server.SourceMap = map[string]settings.Source{
|
||||
"mnt/storage": {
|
||||
Path: "mnt/storage",
|
||||
Name: "storage",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test that explicit DenyAll rule also works when DenyByDefault is enabled
|
||||
if err = s.DenyAll("mnt/storage", "/restricted"); err != nil {
|
||||
t.Errorf("DenyAll failed: %v", err)
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/restricted", "alice") {
|
||||
t.Error("alice should not be permitted (DenyAll rule)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/restricted", "bob") {
|
||||
t.Error("bob should not be permitted (DenyAll rule)")
|
||||
}
|
||||
|
||||
// Test that Allow rule overrides DenyAll even with DenyByDefault
|
||||
if err = s.AllowUser("mnt/storage", "/restricted", "alice"); err != nil {
|
||||
t.Errorf("AllowUser failed: %v", err)
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/restricted", "alice") {
|
||||
t.Error("alice should be permitted (allow overrides DenyAll)")
|
||||
}
|
||||
|
||||
// Remove DenyAll and test that DenyByDefault takes effect
|
||||
removed, err := s.RemoveDenyAll("mnt/storage", "/restricted")
|
||||
if err != nil {
|
||||
t.Errorf("RemoveDenyAll failed: %v", err)
|
||||
}
|
||||
if !removed {
|
||||
t.Error("RemoveDenyAll should have removed the rule")
|
||||
}
|
||||
|
||||
// After removing DenyAll, alice should be permitted due to Allow rule
|
||||
if !s.Permitted("mnt/storage", "/restricted", "alice") {
|
||||
t.Error("alice should be permitted after removing DenyAll (has allow rule)")
|
||||
}
|
||||
// Bob should not be permitted due to DenyByDefault
|
||||
if s.Permitted("mnt/storage", "/restricted", "bob") {
|
||||
t.Error("bob should not be permitted (DenyByDefault is true and no allow rule)")
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ type GroupListResponse struct {
|
||||
// @Param path query string false "Index path (e.g. /secret)"
|
||||
// @Param user query string false "Username to filter rules for"
|
||||
// @Param group query string false "Group name to filter rules for"
|
||||
// @Success 200 {object} object "Varies based on query. Can be access.FrontendAccessRule, []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule"
|
||||
// @Success 200 {object} object "Varies based on query. Can be access.FrontendAccessRule (when source and path specified), []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule"
|
||||
// @Failure 400 {object} map[string]string "Bad request"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/access [get]
|
||||
|
||||
@@ -56,7 +56,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Varies based on query. Can be access.FrontendAccessRule, []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule",
|
||||
"description": "Varies based on query. Can be access.FrontendAccessRule (when source and path specified), []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
@@ -2985,6 +2985,10 @@ const docTemplate = `{
|
||||
"description": "default \"/\" should match folders under path",
|
||||
"type": "string"
|
||||
},
|
||||
"denyByDefault": {
|
||||
"description": "deny access unless an \"allow\" access rule was specifically created.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableIndexing": {
|
||||
"description": "disable the indexing of this source",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Varies based on query. Can be access.FrontendAccessRule, []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule",
|
||||
"description": "Varies based on query. Can be access.FrontendAccessRule (when source and path specified), []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
@@ -2974,6 +2974,10 @@
|
||||
"description": "default \"/\" should match folders under path",
|
||||
"type": "string"
|
||||
},
|
||||
"denyByDefault": {
|
||||
"description": "deny access unless an \"allow\" access rule was specifically created.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableIndexing": {
|
||||
"description": "disable the indexing of this source",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -515,6 +515,9 @@ definitions:
|
||||
defaultUserScope:
|
||||
description: default "/" should match folders under path
|
||||
type: string
|
||||
denyByDefault:
|
||||
description: deny access unless an "allow" access rule was specifically created.
|
||||
type: boolean
|
||||
disableIndexing:
|
||||
description: disable the indexing of this source
|
||||
type: boolean
|
||||
@@ -1083,8 +1086,9 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Varies based on query. Can be access.FrontendAccessRule, []access.PrincipalRule,
|
||||
map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule
|
||||
description: Varies based on query. Can be access.FrontendAccessRule (when
|
||||
source and path specified), []access.PrincipalRule, map[string][]access.PrincipalRule,
|
||||
or map[string]access.FrontendAccessRule
|
||||
schema:
|
||||
type: object
|
||||
"400":
|
||||
|
||||
@@ -21,6 +21,7 @@ server:
|
||||
- path: "/relative/or/absolute/path" # file system path. (Can be relative) validate:required
|
||||
name: "backend" # display name
|
||||
config:
|
||||
denyByDefault: false # deny access unless an "allow" access rule was specifically created.
|
||||
private: false # designate as source as private -- currently just means no sharing permitted.
|
||||
disabled: false # disable the source, this is useful so you don't need to remove it from the config file
|
||||
indexingIntervalMinutes: 0 # optional manual overide interval in minutes to re-index the source
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="inputWrapper" style="display: flex">
|
||||
<select
|
||||
v-if="multipleSources"
|
||||
class="searchContext input"
|
||||
class="searchContext button input"
|
||||
aria-label="search-path"
|
||||
v-model="selectedSource"
|
||||
:value="selectedSource"
|
||||
@@ -538,7 +538,10 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.sizeInputWrapper {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.main-input {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -551,7 +554,6 @@ export default {
|
||||
border-left: 1px solid gray;
|
||||
border-right: 1px solid gray;
|
||||
word-wrap: break-word;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.searchContext.input {
|
||||
@@ -562,7 +564,6 @@ export default {
|
||||
width: 25%;
|
||||
min-width: 7em;
|
||||
max-width: 15em;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
#results > #result-list {
|
||||
|
||||
@@ -16,11 +16,24 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $t("prompts.source", { suffix: ":" }) }} {{ currentSource }}</p>
|
||||
<div aria-label="access-path" class="searchContext clickable" @click="startPathEdit">
|
||||
<div aria-label="access-path" class="searchContext clickable button" @click="startPathEdit">
|
||||
{{ $t("search.path") }} {{ currentPath }}
|
||||
</div>
|
||||
<!-- Default behavior banner -->
|
||||
<div class="card item">
|
||||
<div class="card-content banner-content">
|
||||
<i class="material-icons">{{ sourceDenyDefault ? 'block' : 'check_circle' }}</i> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
{{ $t("access.defaultBehavior", { suffix: ":" }) }} {{ sourceDenyDefault ? $t("access.deny") : $t("access.allow")
|
||||
}}
|
||||
<i class="no-select material-symbols-outlined tooltip-info-icon"
|
||||
@mouseenter="showTooltip($event, $t('access.defaultBehaviorDescription'))" @mouseleave="hideTooltip">
|
||||
help
|
||||
</i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Add Form -->
|
||||
<div class="form-flex-group" >
|
||||
<div class="form-flex-group">
|
||||
<select class="input flat-right form-compact" v-model="addType">
|
||||
<option value="user">{{ $t("general.user") }}</option>
|
||||
<option value="group">{{ $t("general.group") }}</option>
|
||||
@@ -30,7 +43,8 @@
|
||||
<option value="deny">{{ $t("access.deny") }}</option>
|
||||
<option value="allow">{{ $t("access.allow") }}</option>
|
||||
</select>
|
||||
<input v-if="addType !== 'all'" class="input flat-right flat-left form-grow form-compact" v-model="addName" :placeholder="$t('access.enterName')" list="group-suggestions" />
|
||||
<input v-if="addType !== 'all'" class="input flat-right flat-left form-grow form-compact" v-model="addName"
|
||||
:placeholder="$t('access.enterName')" list="group-suggestions" />
|
||||
<datalist id="group-suggestions">
|
||||
<option v-for="group in groups" :key="group" :value="group"></option>
|
||||
</datalist>
|
||||
@@ -48,7 +62,8 @@
|
||||
</tr>
|
||||
<tr v-for="entry in entries" :key="entry.type + '-' + entry.name">
|
||||
<td>{{ entry.allow ? $t("access.allow") : $t("access.deny") }}</td>
|
||||
<td>{{ entry.type === 'user' ? $t("general.user") : (entry.type === 'group' ? $t("general.group") : $t('access.all')) }}</td>
|
||||
<td>{{ entry.type === 'user' ? $t("general.user") : (entry.type === 'group' ? $t("general.group") :
|
||||
$t('access.all')) }}</td>
|
||||
<td>{{ entry.name }}</td>
|
||||
<td>
|
||||
<button @click="deleteAccess(entry)" class="action" :aria-label="$t('buttons.delete')"
|
||||
@@ -62,11 +77,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button v-if="!isEditingPath" @click="closeHovers" class="button button--flat button--grey" :aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">
|
||||
{{ $t("buttons.close") }}
|
||||
</button>
|
||||
</div>
|
||||
<button v-if="!isEditingPath" @click="closeHovers" class="button button--flat button--grey"
|
||||
:aria-label="$t('buttons.close')" :title="$t('buttons.close')">
|
||||
{{ $t("buttons.close") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -90,6 +105,7 @@ export default {
|
||||
currentSource: this.sourceName,
|
||||
tempSource: this.sourceName,
|
||||
rule: { denyAll: false, deny: { users: [], groups: [] }, allow: { users: [], groups: [] } },
|
||||
sourceDenyDefault: false,
|
||||
addType: "user",
|
||||
addListType: "deny",
|
||||
addName: "",
|
||||
@@ -171,10 +187,14 @@ export default {
|
||||
},
|
||||
async fetchRule() {
|
||||
try {
|
||||
this.rule = await accessApi.get(this.currentSource, this.currentPath);
|
||||
const response = await accessApi.get(this.currentSource, this.currentPath);
|
||||
// Handle new API response structure - now sourceDenyDefault is part of the rule
|
||||
this.rule = response;
|
||||
this.sourceDenyDefault = response.sourceDenyDefault || false;
|
||||
} catch (e) {
|
||||
notify.showError(e);
|
||||
this.rule = { denyAll: false, deny: { users: [], groups: [] }, allow: { users: [], groups: [] } };
|
||||
this.sourceDenyDefault = false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -225,6 +245,16 @@ export default {
|
||||
} else {
|
||||
this.$emit('close');
|
||||
}
|
||||
},
|
||||
showTooltip(event, text) {
|
||||
mutations.showTooltip({
|
||||
content: text,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
},
|
||||
hideTooltip() {
|
||||
mutations.hideTooltip();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -234,4 +264,16 @@ export default {
|
||||
.form-flex-group {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.25em !important;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.searchContext {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<!-- Duration Input -->
|
||||
<p>{{ $t('api.tokenDuration') }}</p>
|
||||
<div class="inputWrapper">
|
||||
<div class="sizeInputWrapper">
|
||||
<input class="sizeInput roundedInputLeft input" v-model.number="duration" type="number" min="1"
|
||||
:placeholder="$t('api.durationNumberPlaceholder')" />
|
||||
<select v-model="unit" class="roundedInputRight input">
|
||||
@@ -97,3 +97,8 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.sizeInputWrapper {
|
||||
display: flex !important;
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
<p v-else>
|
||||
{{ $t("prompts.deleteMessageMultiple", { count: selectedCount }) }}
|
||||
</p>
|
||||
<div style="display: grid" aria-label="delete-path" class="searchContext">
|
||||
<div style="display: grid" aria-label="delete-path" class="searchContext button">
|
||||
<span v-for="(item, index) in nav" :key="index"> {{ item }} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
|
||||
<div>Source: {{ sourcePath.source }} </div> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<div aria-label="filelist-path" class="searchContext clickable">{{$t('search.path')}} {{ sourcePath.path }}</div>
|
||||
<div aria-label="filelist-path" class="searchContext button clickable">{{$t('search.path')}} {{ sourcePath.path }}</div>
|
||||
<ul class="file-list">
|
||||
<li
|
||||
@click="itemClick"
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("general.password") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div v-if="showWrongCredentials" class="share__wrong__password">
|
||||
{{ $t("login.wrongCredentials") }}
|
||||
</div>
|
||||
<div class="form-flex-group">
|
||||
<input
|
||||
class="input share-password"
|
||||
v-focus
|
||||
type="password"
|
||||
:placeholder="$t('general.password')"
|
||||
v-model="password"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.submit')"
|
||||
:title="$t('buttons.submit')"
|
||||
>
|
||||
{{ $t("buttons.submit") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mutations } from "@/store";
|
||||
|
||||
export default {
|
||||
name: "password",
|
||||
props: {
|
||||
submitCallback: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
showWrongCredentials: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
initialPassword: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
password: this.initialPassword,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
mutations.closeHovers();
|
||||
this.submitCallback(this.password);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.share__wrong__password {
|
||||
color: #ff4757;
|
||||
text-align: center;
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
.share-password {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -30,6 +30,7 @@ import UserEdit from "./UserEdit.vue";
|
||||
import buttons from "@/utils/buttons";
|
||||
import Totp from "./Totp.vue";
|
||||
import Access from "./Access.vue";
|
||||
import Password from "./Password.vue";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
|
||||
export default {
|
||||
@@ -56,6 +57,7 @@ export default {
|
||||
CreateApi,
|
||||
ActionApi,
|
||||
Access,
|
||||
Password,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h2>{{ $t("buttons.share") }}</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div aria-label="share-path" class="searchContext"> {{ $t('search.path') }} {{ item.path }}</div>
|
||||
<div aria-label="share-path" class="searchContext button"> {{ $t('search.path') }} {{ item.path }}</div>
|
||||
<p> {{ $t('share.notice') }} </p>
|
||||
|
||||
<div v-if="listing">
|
||||
|
||||
@@ -179,13 +179,13 @@ input.sizeInput:disabled {
|
||||
border-radius: 1em;
|
||||
margin: .5em;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
padding: .25em;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
border: 1px solid #ccc;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ input.sizeInput:disabled {
|
||||
backdrop-filter: invert(.1);
|
||||
height: 2em;
|
||||
width: 5em;
|
||||
|
||||
}
|
||||
|
||||
.roundedInputLeft {
|
||||
@@ -206,10 +207,10 @@ input.sizeInput:disabled {
|
||||
border-top-right-radius: 0;
|
||||
width: auto;
|
||||
background-color: var(--surfaceSecondary);
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
.roundedInputRight {
|
||||
padding: 1em;
|
||||
border-radius: 1em;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
|
||||
@@ -495,7 +495,9 @@
|
||||
"deleted": "Access rule deleted!",
|
||||
"added": "Access rule added!",
|
||||
"name": "Name",
|
||||
"accessManagement": "Access Management"
|
||||
"accessManagement": "Access Management",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "This shows what happens when a user tries to access a path where no specific access rules exist. 'Allow' means users can access by default, 'Deny' means users are blocked by default unless explicitly allowed."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Uploads & Downloads",
|
||||
|
||||
@@ -10,36 +10,7 @@
|
||||
/>
|
||||
|
||||
<breadcrumbs v-if="showBreadCrumbs" :base="isShare ? `/share/${shareHash}` : undefined" />
|
||||
<!-- Share password prompt -->
|
||||
<div v-if="isShare && error && error.status === 401" class="card floating" id="password">
|
||||
<div v-if="attemptedPasswordLogin" class="share__wrong__password">
|
||||
{{ $t("login.wrongCredentials") }}
|
||||
</div>
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("general.password") }}</h2>
|
||||
</div>
|
||||
<div class="card-content form-flex-group">
|
||||
<input
|
||||
class="input share-password"
|
||||
v-focus
|
||||
type="password"
|
||||
:placeholder="$t('general.password')"
|
||||
v-model="sharePassword"
|
||||
@keyup.enter="fetchData"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="fetchData"
|
||||
:aria-label="$t('buttons.submit')"
|
||||
:title="$t('buttons.submit')"
|
||||
>
|
||||
{{ $t("buttons.submit") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<errors v-else-if="error" :errorCode="error.status" />
|
||||
<errors v-if="error && !(isShare && error.status === 401)" :errorCode="error.status" />
|
||||
<component v-else-if="currentViewLoaded" :is="currentView"></component>
|
||||
<div v-else>
|
||||
<h2 class="message delayed">
|
||||
@@ -214,6 +185,7 @@ export default {
|
||||
} else if (e.status === 401 && this.isShare) {
|
||||
// Handle share password requirement
|
||||
this.attemptedPasswordLogin = this.sharePassword !== "";
|
||||
this.showPasswordPrompt();
|
||||
} else {
|
||||
router.push({ name: "error" });
|
||||
}
|
||||
@@ -329,6 +301,19 @@ export default {
|
||||
mutations.setLoading("files", false);
|
||||
}
|
||||
},
|
||||
showPasswordPrompt() {
|
||||
mutations.showHover({
|
||||
name: "password",
|
||||
props: {
|
||||
submitCallback: (password) => {
|
||||
this.sharePassword = password;
|
||||
this.fetchData();
|
||||
},
|
||||
showWrongCredentials: this.attemptedPasswordLogin,
|
||||
initialPassword: this.sharePassword,
|
||||
},
|
||||
});
|
||||
},
|
||||
keyEvent(event) {
|
||||
// F1!
|
||||
if (event.keyCode === 112) {
|
||||
@@ -348,17 +333,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.share__wrong__password {
|
||||
color: #ff4757;
|
||||
text-align: center;
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
#password {
|
||||
max-width: 400px;
|
||||
margin: 2em auto;
|
||||
}
|
||||
|
||||
.scroll-glow {
|
||||
animation: scrollGlowAnimation 1s ease-out;
|
||||
}
|
||||
@@ -375,9 +349,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.share-password {
|
||||
width: 100%;
|
||||
}
|
||||
.share-info-component {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
@@ -158,7 +158,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
tr > td {
|
||||
tr > td,
|
||||
tr > th {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user