diff --git a/README.md b/README.md index be02a5d..bc3aac4 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ The program: - If user creation fails because admin rights are needed, explains the issue and offers to retry with `sudo`. - After creating a user, offers to launch `smbpasswd -a ` immediately so the Samba password can be set right away. - Before saving, checks whether each share folder exists and offers to create missing directories. +- Before saving, offers to adjust share-folder ownership and permissions so connected users can actually read and write files. - If saving the Samba config or its backup needs admin rights, explains the issue and offers to retry with `sudo`. - Writes a timestamped backup before saving changes. diff --git a/app.go b/app.go index ca52c44..92cff65 100644 --- a/app.go +++ b/app.go @@ -273,6 +273,9 @@ func (a *App) ensureShareDirectories(doc *Document) error { info, err := os.Stat(cfg.Path) if err == nil { if info.IsDir() { + if err := a.offerAdjustDirectoryPermissions(cfg); err != nil { + return err + } continue } return fmt.Errorf("share %q path exists but is not a directory: %s", cfg.Name, cfg.Path) @@ -301,6 +304,9 @@ func (a *App) ensureShareDirectories(doc *Document) error { } a.printf("Created folder %s\n", cfg.Path) + if err := a.offerAdjustDirectoryPermissions(cfg); err != nil { + return err + } } return nil @@ -693,6 +699,124 @@ func (a *App) createDirectory(path string) error { } } +func (a *App) offerAdjustDirectoryPermissions(cfg ShareConfig) error { + mode, owner, group, explanation := recommendSharePermissions(cfg) + + a.println("") + a.printf("The share folder %s may need permissions adjusted for file operations.\n", cfg.Path) + a.println(explanation) + if owner != "" || group != "" { + a.printf("Suggested owner/group: %s:%s\n", defaultString(owner, "unchanged"), defaultString(group, "unchanged")) + } + a.printf("Suggested permissions: %s\n", mode) + a.println("I can apply these recommended permissions now.") + a.flush() + + adjust, err := a.confirm("Adjust this folder now", true) + if err != nil { + return err + } + if !adjust { + return nil + } + + if err := a.applyDirectoryPermissions(cfg.Path, owner, group, mode); err != nil { + return fmt.Errorf("adjust permissions for share %q: %w", cfg.Name, err) + } + + a.printf("Updated permissions for %s\n", cfg.Path) + return nil +} + +func recommendSharePermissions(cfg ShareConfig) (mode, owner, group, explanation string) { + mode = "0770" + group = "users" + explanation = "Members of the allowed account list should usually be able to read and write here." + + if strings.EqualFold(normalizeBoolish(cfg.ReadOnly, "no"), "yes") { + mode = "0755" + explanation = "This share is marked read-only, so a safer folder mode is recommended." + } + + if strings.EqualFold(normalizeBoolish(cfg.GuestOK, "no"), "yes") { + mode = "0777" + group = "" + explanation = "This share allows guests, so wider permissions are usually needed for uploads and edits." + return mode, "", group, explanation + } + + if len(cfg.ValidUsers) == 1 { + owner = cfg.ValidUsers[0] + group = cfg.ValidUsers[0] + explanation = "This share is limited to one account, so making that account the owner is the simplest setup." + return mode, owner, group, explanation + } + + if len(cfg.ValidUsers) > 1 { + explanation = "This share has multiple allowed accounts, so a shared writable group-style setup is recommended." + } + + return mode, owner, group, explanation +} + +func (a *App) applyDirectoryPermissions(path, owner, group, mode string) error { + chownTarget := owner + if group != "" { + if chownTarget == "" { + chownTarget = ":" + group + } else { + chownTarget = chownTarget + ":" + group + } + } + + if err := a.runPrivilegedOrLocal("chmod", []string{mode, path}, "Changing folder permissions needs administrator permission."); err != nil { + return err + } + + if chownTarget != "" { + if err := a.runPrivilegedOrLocal("chown", []string{chownTarget, path}, "Changing folder ownership needs administrator permission."); err != nil { + return err + } + } + + return nil +} + +func (a *App) runPrivilegedOrLocal(command string, args []string, privilegeMessage string) error { + if err := a.runner.Run(command, args...); err == nil { + return nil + } else if shouldOfferPrivilegeRetry(err) { + a.println("") + a.println(privilegeMessage) + a.println("I can try again using sudo so you can enter your admin password.") + a.flush() + + canUseSudo := false + if a.lookPath != nil { + _, sudoErr := a.lookPath("sudo") + canUseSudo = sudoErr == nil + } + if !canUseSudo { + return fmt.Errorf("%s %s: %w", command, strings.Join(args, " "), err) + } + + retry, promptErr := a.confirm("Retry with sudo", true) + if promptErr != nil { + return promptErr + } + if !retry { + return fmt.Errorf("%s %s: %w", command, strings.Join(args, " "), err) + } + + if sudoErr := a.runner.Run("sudo", append([]string{command}, args...)...); sudoErr != nil { + return fmt.Errorf("%s %s with sudo: %w", command, strings.Join(args, " "), sudoErr) + } + return nil + } else { + return fmt.Errorf("%s %s: %w", command, strings.Join(args, " "), err) + } +} + func friendlyWriteError(action, path string, err error) error { if errors.Is(err, os.ErrPermission) || strings.Contains(strings.ToLower(err.Error()), "permission denied") { return fmt.Errorf("%s %s: administrator permission is needed", action, path) diff --git a/samba-configer b/samba-configer index 8cf4adf..3e41694 100755 Binary files a/samba-configer and b/samba-configer differ