package main import ( "bufio" "errors" "fmt" "os" "os/exec" "os/user" "path/filepath" "slices" "strconv" "strings" "time" ) var ErrCancelled = errors.New("cancelled") type UserManager interface { UserExists(name string) bool } type CommandRunner interface { Run(name string, args ...string) error } type LookPathFunc func(file string) (string, error) type RealUserManager struct{} func (RealUserManager) UserExists(name string) bool { _, err := user.Lookup(name) return err == nil } type OSCommandRunner struct{} func (OSCommandRunner) Run(name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } type App struct { configPath string users UserManager runner CommandRunner lookPath LookPathFunc reader *bufio.Reader writer *bufio.Writer } type PasswdEntry struct { Name string UID int GID int Home string Shell string } func NewApp(configPath string, users UserManager, runner CommandRunner, lookPath LookPathFunc) *App { return &App{ configPath: configPath, users: users, runner: runner, lookPath: lookPath, reader: bufio.NewReader(os.Stdin), writer: bufio.NewWriter(os.Stdout), } } func (a *App) Run() error { doc, err := ParseConfigFile(a.configPath) if err != nil { if os.IsNotExist(err) { doc, err = a.handleMissingConfig() if err == nil { goto loaded } } return fmt.Errorf("read config: %w", err) } loaded: for { shareSections := doc.ShareSections() a.println("") a.println("Samba share editor") a.println("==================") a.println("Current shares:") if len(shareSections) == 0 { a.println(" (none)") } else { for i, section := range shareSections { cfg := ShareFromSection(section) a.printf(" %d. %s -> %s\n", i+1, cfg.Name, cfg.Path) } } a.println("") a.println("[a] add share") a.println("[e] edit share") a.println("[d] delete share") a.println("[u] manage users") a.println("[w] write config and exit") a.println("[q] quit without saving") a.flush() choice, err := a.prompt("Select an action") if err != nil { return err } switch strings.ToLower(choice) { case "a", "add": if err := a.addShare(doc); err != nil { return err } case "e", "edit": if err := a.editShare(doc); err != nil { return err } case "d", "delete": if err := a.deleteShare(doc); err != nil { return err } case "u", "users": if err := a.manageUsers(doc); err != nil { return err } case "w", "write": return a.writeConfig(doc) case "q", "quit": return nil default: a.println("Unknown choice.") } } } func (a *App) manageUsers(doc *Document) error { for { a.println("") a.println("User management") a.println("===============") a.println("[c] check share accounts") a.println("[p] change a Samba password") a.println("[x] delete an unused account") a.println("[b] back") a.flush() choice, err := a.prompt("Select an action") if err != nil { return err } switch strings.ToLower(choice) { case "c", "check": if err := a.checkShareAccounts(doc); err != nil { return err } case "p", "password": if err := a.changeSambaPassword(doc); err != nil { return err } case "x", "delete": if err := a.deleteUnusedAccount(doc); err != nil { return err } case "b", "back": return nil default: a.println("Unknown choice.") } } } func (a *App) checkShareAccounts(doc *Document) error { references := shareUserReferences(doc) if len(references) == 0 { a.println("No accounts are listed in any share.") return nil } a.println("") a.println("Accounts used by the current shares:") for _, ref := range references { status := "missing" if a.users.UserExists(ref.User) { status = "present" } a.printf(" %s [%s] used by: %s\n", ref.User, status, strings.Join(ref.Shares, ", ")) } var missing []string for _, ref := range references { if !a.users.UserExists(ref.User) { missing = append(missing, ref.User) } } if len(missing) == 0 { a.println("") a.println("All listed share accounts already exist.") return nil } a.println("") a.println("Some accounts are missing and those users may not be able to sign in.") if err := a.ensureUsers(missing); err != nil { return err } return nil } func (a *App) deleteUnusedAccount(doc *Document) error { entries, err := readPasswdEntries() if err != nil { return fmt.Errorf("read local users: %w", err) } candidates := unusedAccountCandidates(entries, shareUsers(doc)) if len(candidates) == 0 { a.println("I couldn't find any obvious unused share accounts to delete.") a.println("For safety, system accounts are excluded from this list.") return nil } a.println("") a.println("Accounts that look unused by the current shares:") for i, entry := range candidates { a.printf(" %d. %s (shell: %s)\n", i+1, entry.Name, entry.Shell) } a.println("Only accounts that are not listed in any share are shown here.") a.flush() raw, err := a.prompt("Account number to delete") if err != nil { return err } index, err := strconv.Atoi(raw) if err != nil || index < 1 || index > len(candidates) { a.println("Invalid selection.") return nil } entry := candidates[index-1] a.println("") a.printf("This will remove the local account %q.\n", entry.Name) a.println("This is only safe if nobody needs this account for Samba or anything else.") a.flush() confirm, err := a.confirm("Delete this account now", false) if err != nil { return err } if !confirm { a.println("Delete cancelled.") return nil } if err := a.deleteUser(entry.Name); err != nil { return err } a.printf("Deleted account %s\n", entry.Name) return nil } func (a *App) changeSambaPassword(doc *Document) error { if a.lookPath != nil { if _, err := a.lookPath("smbpasswd"); err != nil { a.println("I couldn't find the smbpasswd tool yet.") a.println("Install Samba first, then try this again.") return nil } } references := shareUserReferences(doc) if len(references) == 0 { a.println("No accounts are listed in any share.") return nil } a.println("") a.println("Accounts used by the current shares:") for i, ref := range references { status := "missing" if a.users.UserExists(ref.User) { status = "present" } a.printf(" %d. %s [%s] used by: %s\n", i+1, ref.User, status, strings.Join(ref.Shares, ", ")) } a.flush() raw, err := a.prompt("Account number to update") if err != nil { return err } index, err := strconv.Atoi(raw) if err != nil || index < 1 || index > len(references) { a.println("Invalid selection.") return nil } ref := references[index-1] if !a.users.UserExists(ref.User) { a.println("") a.printf("The local account %q does not exist yet.\n", ref.User) a.println("I can create it first so a Samba password can be set.") a.flush() create, promptErr := a.confirm("Create this account now", true) if promptErr != nil { return promptErr } if !create { return nil } if err := a.createUser(ref.User); err != nil { return err } } a.println("") a.printf("I’ll open the password setup for %q now.\n", ref.User) a.println("You’ll be asked to type the new Samba password.") a.flush() return a.setSambaPassword(ref.User) } func (a *App) handleMissingConfig() (*Document, error) { a.println("") a.println("Samba is not set up yet on this computer.") a.printf("I couldn't find the Samba config file at %s.\n", a.configPath) plan, ok := DetectSambaInstallPlan(a.lookPath, os.Geteuid() == 0) if !ok { a.println("I also couldn't find a supported package manager automatically.") a.println("Install the Samba server package for your Linux distribution, then run this tool again.") a.flush() return nil, os.ErrNotExist } a.println("") a.printf("I found %s and can try to install the Samba server for you.\n", plan.ManagerName) a.printf("This would run: %s\n", plan.DisplayCommand()) a.println("You may be asked for your administrator password.") a.flush() install, err := a.confirm("Install Samba server now", true) if err != nil { return nil, err } if !install { return nil, os.ErrNotExist } if err := a.runner.Run(plan.Command[0], plan.Command[1:]...); err != nil { return nil, fmt.Errorf("install Samba with %s: %w", plan.ManagerName, err) } doc, err := ParseConfigFile(a.configPath) if err != nil { return nil, fmt.Errorf("Samba installation finished, but the config file is still missing at %s: %w", a.configPath, err) } a.println("Samba was installed and the config file is now available.") a.flush() return doc, nil } func (a *App) addShare(doc *Document) error { cfg, err := a.collectShareConfig(ShareConfig{}) if err != nil { return err } if doc.Section(cfg.Name) != nil { a.println("A share with that name already exists.") return nil } if err := a.ensureUsers(cfg.ValidUsers); err != nil { return err } doc.UpsertSection(BuildShareSection(nil, cfg)) a.println("Share added.") return nil } func (a *App) editShare(doc *Document) error { section, err := a.selectShare(doc) if err != nil || section == nil { return err } originalName := section.Name cfg, err := a.collectShareConfig(ShareFromSection(section)) if err != nil { return err } if !strings.EqualFold(originalName, cfg.Name) && doc.Section(cfg.Name) != nil { a.println("A share with that new name already exists.") return nil } if err := a.ensureUsers(cfg.ValidUsers); err != nil { return err } if !strings.EqualFold(originalName, cfg.Name) { doc.DeleteSection(originalName) } doc.UpsertSection(BuildShareSection(section, cfg)) a.println("Share updated.") return nil } func (a *App) deleteShare(doc *Document) error { section, err := a.selectShare(doc) if err != nil || section == nil { return err } confirm, err := a.confirm(fmt.Sprintf("Delete share %q", section.Name), false) if err != nil { return err } if !confirm { a.println("Delete cancelled.") return nil } doc.DeleteSection(section.Name) a.println("Share deleted.") return nil } func (a *App) writeConfig(doc *Document) error { if err := a.ensureShareDirectories(doc); err != nil { return err } backup := fmt.Sprintf("%s.bak.%s", a.configPath, time.Now().UTC().Format("20060102T150405Z")) serialized := doc.Serialize() if err := copyFile(a.configPath, backup); err != nil { if shouldOfferPrivilegeRetry(err) { return a.writeConfigWithPrivilegeRetry(serialized, backup) } return friendlyWriteError("create backup", backup, err) } if err := os.WriteFile(a.configPath, []byte(serialized), 0o644); err != nil { if shouldOfferPrivilegeRetry(err) { return a.writeConfigWithPrivilegeRetry(serialized, backup) } return friendlyWriteError("write config", a.configPath, err) } a.printf("Config written to %s\n", a.configPath) a.printf("Backup saved to %s\n", backup) return nil } func (a *App) ensureShareDirectories(doc *Document) error { for _, section := range doc.ShareSections() { cfg := ShareFromSection(section) if strings.TrimSpace(cfg.Path) == "" { continue } 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) } if !os.IsNotExist(err) { return fmt.Errorf("check share %q path %s: %w", cfg.Name, cfg.Path, err) } a.println("") a.printf("The folder for share %q does not exist yet.\n", cfg.Name) a.printf("Missing folder: %s\n", cfg.Path) a.println("I can create it now so the share is ready to use.") a.flush() create, promptErr := a.confirm("Create this folder now", true) if promptErr != nil { return promptErr } if !create { return fmt.Errorf("share %q needs an existing folder before saving", cfg.Name) } if err := a.createDirectory(cfg.Path); err != nil { return fmt.Errorf("create folder for share %q: %w", cfg.Name, err) } a.printf("Created folder %s\n", cfg.Path) if err := a.offerAdjustDirectoryPermissions(cfg); err != nil { return err } } return nil } func (a *App) selectShare(doc *Document) (*Section, error) { shares := doc.ShareSections() if len(shares) == 0 { a.println("There are no shares to select.") return nil, nil } raw, err := a.prompt("Share number") if err != nil { return nil, err } index, err := strconv.Atoi(raw) if err != nil || index < 1 || index > len(shares) { a.println("Invalid selection.") return nil, nil } return shares[index-1], nil } func (a *App) collectShareConfig(existing ShareConfig) (ShareConfig, error) { name, err := a.promptDefault("Share name", existing.Name) if err != nil { return ShareConfig{}, err } name = strings.TrimSpace(name) if name == "" { return ShareConfig{}, errors.New("share name cannot be empty") } path, err := a.promptDefault("Path", existing.Path) if err != nil { return ShareConfig{}, err } path = strings.TrimSpace(path) if path == "" { return ShareConfig{}, errors.New("path cannot be empty") } comment, err := a.promptDefault("Comment", existing.Comment) if err != nil { return ShareConfig{}, err } browseable, err := a.promptDefault("Browseable (yes/no)", defaultString(existing.Browseable, "yes")) if err != nil { return ShareConfig{}, err } readOnly, err := a.promptDefault("Read only (yes/no)", defaultString(existing.ReadOnly, "no")) if err != nil { return ShareConfig{}, err } guestOK, err := a.promptDefault("Guest ok (yes/no)", defaultString(existing.GuestOK, "no")) if err != nil { return ShareConfig{}, err } usersDefault := strings.Join(existing.ValidUsers, ", ") usersRaw, err := a.promptDefault("Valid users (comma or space separated, blank for none)", usersDefault) if err != nil { return ShareConfig{}, err } return ShareConfig{ Name: name, Path: filepath.Clean(path), Comment: strings.TrimSpace(comment), Browseable: browseable, ReadOnly: readOnly, GuestOK: guestOK, ValidUsers: splitUsers(usersRaw), }, nil } func (a *App) ensureUsers(users []string) error { for _, name := range users { if a.users.UserExists(name) { continue } create, err := a.confirm(fmt.Sprintf("User %q does not exist. Create it with useradd", name), true) if err != nil { return err } if !create { continue } if err := a.createUser(name); err != nil { return err } a.printf("Created local user %s\n", name) if err := a.offerSetSambaPassword(name); err != nil { return err } } return nil } func (a *App) createUser(name string) error { args := []string{"-M", "-s", "/usr/sbin/nologin", name} if err := a.runner.Run("useradd", args...); err == nil { return nil } else if shouldOfferPrivilegeRetry(err) { a.println("") a.println("Creating a Linux user usually needs administrator permission.") 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("create user %s: %w", name, err) } retry, promptErr := a.confirm("Retry user creation with sudo", true) if promptErr != nil { return promptErr } if !retry { return fmt.Errorf("create user %s: %w", name, err) } if sudoErr := a.runner.Run("sudo", append([]string{"useradd"}, args...)...); sudoErr != nil { return fmt.Errorf("create user %s with sudo: %w", name, sudoErr) } return nil } else { return fmt.Errorf("create user %s: %w", name, err) } } func (a *App) offerSetSambaPassword(name string) error { a.println("") a.println("This share can use a Samba password for sign-in.") a.println("I can set that up now and ask you to type the password you want to use.") a.flush() setPassword, err := a.confirm(fmt.Sprintf("Set a Samba password for %q now", name), true) if err != nil { return err } if !setPassword { a.println("You can do this later with: smbpasswd -a " + name) return nil } return a.setSambaPassword(name) } func (a *App) setSambaPassword(name string) error { if a.lookPath != nil { if _, err := a.lookPath("smbpasswd"); err != nil { a.println("I couldn't find the smbpasswd tool yet.") a.println("If Samba was just installed, try opening the app again after the installation finishes.") return fmt.Errorf("set Samba password for %s: smbpasswd command not found", name) } } if err := a.runner.Run("smbpasswd", "-a", name); err == nil { a.printf("Samba password set for %s\n", name) return nil } else if shouldOfferPrivilegeRetry(err) { a.println("") a.println("Setting a Samba password usually needs administrator permission.") 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("set Samba password for %s: %w", name, err) } retry, promptErr := a.confirm("Retry Samba password setup with sudo", true) if promptErr != nil { return promptErr } if !retry { return fmt.Errorf("set Samba password for %s: %w", name, err) } if sudoErr := a.runner.Run("sudo", "smbpasswd", "-a", name); sudoErr != nil { return fmt.Errorf("set Samba password for %s with sudo: %w", name, sudoErr) } a.printf("Samba password set for %s\n", name) return nil } else { return fmt.Errorf("set Samba password for %s: %w", name, err) } } func (a *App) deleteUser(name string) error { if err := a.runPrivilegedOrLocal("userdel", []string{name}, "Deleting a Linux user needs administrator permission."); err != nil { return fmt.Errorf("delete user %s: %w", name, err) } if a.lookPath != nil { if _, err := a.lookPath("smbpasswd"); err == nil { removeSamba, promptErr := a.confirm("Also remove the Samba password for this account", true) if promptErr != nil { return promptErr } if removeSamba { if err := a.runPrivilegedOrLocal("smbpasswd", []string{"-x", name}, "Removing the Samba password needs administrator permission."); err != nil { return fmt.Errorf("remove Samba password for %s: %w", name, err) } } } } return nil } func (a *App) prompt(label string) (string, error) { a.printf("%s: ", label) a.flush() text, err := a.reader.ReadString('\n') if err != nil { return "", err } return strings.TrimSpace(text), nil } func (a *App) promptDefault(label, defaultValue string) (string, error) { if defaultValue == "" { return a.prompt(label) } a.printf("%s [%s]: ", label, defaultValue) a.flush() text, err := a.reader.ReadString('\n') if err != nil { return "", err } text = strings.TrimSpace(text) if text == "" { return defaultValue, nil } return text, nil } func (a *App) confirm(label string, defaultYes bool) (bool, error) { suffix := "y/N" if defaultYes { suffix = "Y/n" } answer, err := a.prompt(fmt.Sprintf("%s [%s]", label, suffix)) if err != nil { return false, err } if answer == "" { return defaultYes, nil } switch strings.ToLower(answer) { case "y", "yes": return true, nil case "n", "no": return false, nil default: return false, errors.New("expected yes or no") } } func (a *App) println(line string) { fmt.Fprintln(a.writer, line) } func (a *App) printf(format string, args ...any) { fmt.Fprintf(a.writer, format, args...) } func (a *App) flush() { _ = a.writer.Flush() } func defaultString(value, fallback string) string { if strings.TrimSpace(value) == "" { return fallback } return value } func copyFile(src, dst string) error { data, err := os.ReadFile(src) if err != nil { return err } return os.WriteFile(dst, data, 0o644) } func shouldOfferPrivilegeRetry(err error) bool { if os.Geteuid() == 0 { return false } text := strings.ToLower(err.Error()) return strings.Contains(text, "permission denied") || strings.Contains(text, "operation not permitted") || strings.Contains(text, "exit status") } func (a *App) writeConfigWithPrivilegeRetry(serialized, backup string) error { a.println("") a.println("Saving Samba settings usually needs administrator permission.") 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 friendlyWriteError("save Samba settings", a.configPath, os.ErrPermission) } retry, err := a.confirm("Retry saving with sudo", true) if err != nil { return err } if !retry { return friendlyWriteError("save Samba settings", a.configPath, os.ErrPermission) } tempPath, err := a.writeTempConfig(serialized) if err != nil { return fmt.Errorf("prepare config for sudo save: %w", err) } defer os.Remove(tempPath) if err := a.runner.Run("sudo", "cp", a.configPath, backup); err != nil { return friendlyWriteError("create backup", backup, err) } if err := a.runner.Run("sudo", "install", "-m", "644", tempPath, a.configPath); err != nil { return friendlyWriteError("write config", a.configPath, err) } a.printf("Config written to %s\n", a.configPath) a.printf("Backup saved to %s\n", backup) return nil } func (a *App) writeTempConfig(serialized string) (string, error) { file, err := os.CreateTemp("", "samba-configer-*.conf") if err != nil { return "", err } defer file.Close() if _, err := file.WriteString(serialized); err != nil { return "", err } return file.Name(), nil } func (a *App) createDirectory(path string) error { if err := os.MkdirAll(path, 0o755); err == nil { return nil } else if shouldOfferPrivilegeRetry(err) { a.println("") a.println("Creating that folder needs administrator permission.") 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("create folder %s: %w", path, err) } retry, promptErr := a.confirm("Retry folder creation with sudo", true) if promptErr != nil { return promptErr } if !retry { return fmt.Errorf("create folder %s: %w", path, err) } if sudoErr := a.runner.Run("sudo", "mkdir", "-p", path); sudoErr != nil { return fmt.Errorf("create folder %s with sudo: %w", path, sudoErr) } if sudoErr := a.runner.Run("sudo", "chmod", "755", path); sudoErr != nil { return fmt.Errorf("set permissions on folder %s with sudo: %w", path, sudoErr) } return nil } else { return fmt.Errorf("create folder %s: %w", path, err) } } 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) } return fmt.Errorf("%s %s: %w", action, path, err) } type ShareUserReference struct { User string Shares []string } func shareUsers(doc *Document) map[string]struct{} { users := make(map[string]struct{}) for _, section := range doc.ShareSections() { cfg := ShareFromSection(section) for _, user := range cfg.ValidUsers { users[user] = struct{}{} } } return users } func shareUserReferences(doc *Document) []ShareUserReference { refs := map[string][]string{} for _, section := range doc.ShareSections() { cfg := ShareFromSection(section) for _, user := range cfg.ValidUsers { refs[user] = append(refs[user], cfg.Name) } } var out []ShareUserReference for user, shares := range refs { out = append(out, ShareUserReference{User: user, Shares: shares}) } slices.SortFunc(out, func(a, b ShareUserReference) int { return strings.Compare(a.User, b.User) }) return out } func readPasswdEntries() ([]PasswdEntry, error) { data, err := os.ReadFile("/etc/passwd") if err != nil { return nil, err } var entries []PasswdEntry for _, line := range strings.Split(string(data), "\n") { if strings.TrimSpace(line) == "" { continue } parts := strings.Split(line, ":") if len(parts) < 7 { continue } uid, err := strconv.Atoi(parts[2]) if err != nil { continue } gid, err := strconv.Atoi(parts[3]) if err != nil { continue } entries = append(entries, PasswdEntry{ Name: parts[0], UID: uid, GID: gid, Home: parts[5], Shell: parts[6], }) } return entries, nil } func unusedAccountCandidates(entries []PasswdEntry, active map[string]struct{}) []PasswdEntry { var candidates []PasswdEntry for _, entry := range entries { if _, ok := active[entry.Name]; ok { continue } if !looksLikeSafeShareAccount(entry) { continue } candidates = append(candidates, entry) } slices.SortFunc(candidates, func(a, b PasswdEntry) int { return strings.Compare(a.Name, b.Name) }) return candidates } func looksLikeSafeShareAccount(entry PasswdEntry) bool { if entry.UID < 1000 { return false } shell := strings.TrimSpace(entry.Shell) return strings.HasSuffix(shell, "/nologin") || strings.HasSuffix(shell, "/false") }