feat: add invalid_users directive for Samba shares
Samba shares now support an 'invalid users' list (deny list), written as 'invalid users = u1,u2' in smb.conf. The UI shows a ChipPicker for valid_users and invalid_users, mutually exclusive, sourced from the system user list. feat: add ImportSystemUsers for fresh installations When NASCTL_IMPORT_ON_BOOT=true, nasctl now imports existing system users from /etc/passwd (UID 1000-60000) and /etc/group (supplemental groups), and detects which have Samba accounts via 'pdbedit -L'. Imported users are marked dirty so the admin can review before applying. New POST /api/import/users endpoint for manual re-import. This mirrors the existing import-on-boot flow for smb.conf and /etc/exports.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
ALTER TABLE samba_shares ADD COLUMN invalid_users TEXT NOT NULL DEFAULT '[]';
|
||||
+11
-10
@@ -25,16 +25,17 @@ type Admin struct {
|
||||
}
|
||||
|
||||
type SambaShare struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Comment string `json:"comment"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
GuestOK bool `json:"guest_ok"`
|
||||
ValidUsers []string `json:"valid_users"`
|
||||
ValidGroups []string `json:"valid_groups"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Comment string `json:"comment"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
GuestOK bool `json:"guest_ok"`
|
||||
ValidUsers []string `json:"valid_users"`
|
||||
ValidGroups []string `json:"valid_groups"`
|
||||
InvalidUsers []string `json:"invalid_users"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type NFSClient struct {
|
||||
|
||||
+21
-8
@@ -42,7 +42,7 @@ func scanSambaShare(row interface {
|
||||
}) (SambaShare, error) {
|
||||
var share SambaShare
|
||||
var readOnly, guestOK int
|
||||
var validUsers, validGroups, createdAt, updatedAt string
|
||||
var validUsers, validGroups, invalidUsers, createdAt, updatedAt string
|
||||
if err := row.Scan(
|
||||
&share.ID,
|
||||
&share.Name,
|
||||
@@ -52,6 +52,7 @@ func scanSambaShare(row interface {
|
||||
&guestOK,
|
||||
&validUsers,
|
||||
&validGroups,
|
||||
&invalidUsers,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
); err != nil {
|
||||
@@ -68,6 +69,10 @@ func scanSambaShare(row interface {
|
||||
if err != nil {
|
||||
return SambaShare{}, err
|
||||
}
|
||||
share.InvalidUsers, err = decodeJSONStrings(invalidUsers)
|
||||
if err != nil {
|
||||
return SambaShare{}, err
|
||||
}
|
||||
share.CreatedAt = parseTime(createdAt)
|
||||
share.UpdatedAt = parseTime(updatedAt)
|
||||
return share, nil
|
||||
@@ -170,7 +175,7 @@ func (d *DB) ListApplyLog(limit int) ([]ApplyLogEntry, error) {
|
||||
|
||||
func (d *DB) ListSambaShares() ([]SambaShare, error) {
|
||||
rows, err := d.conn.Query(`
|
||||
SELECT id, name, path, comment, read_only, guest_ok, valid_users, valid_groups, created_at, updated_at
|
||||
SELECT id, name, path, comment, read_only, guest_ok, valid_users, valid_groups, invalid_users, created_at, updated_at
|
||||
FROM samba_shares ORDER BY name ASC`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list samba shares: %w", err)
|
||||
@@ -190,7 +195,7 @@ func (d *DB) ListSambaShares() ([]SambaShare, error) {
|
||||
|
||||
func (d *DB) GetSambaShare(id int64) (SambaShare, error) {
|
||||
row := d.conn.QueryRow(`
|
||||
SELECT id, name, path, comment, read_only, guest_ok, valid_users, valid_groups, created_at, updated_at
|
||||
SELECT id, name, path, comment, read_only, guest_ok, valid_users, valid_groups, invalid_users, created_at, updated_at
|
||||
FROM samba_shares WHERE id = ?`, id)
|
||||
share, err := scanSambaShare(row)
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -211,6 +216,10 @@ func (d *DB) CreateSambaShare(share SambaShare) (SambaShare, error) {
|
||||
if err != nil {
|
||||
return SambaShare{}, err
|
||||
}
|
||||
invalidUsers, err := encodeJSONStrings(share.InvalidUsers)
|
||||
if err != nil {
|
||||
return SambaShare{}, err
|
||||
}
|
||||
readOnly := 0
|
||||
if share.ReadOnly {
|
||||
readOnly = 1
|
||||
@@ -220,9 +229,9 @@ func (d *DB) CreateSambaShare(share SambaShare) (SambaShare, error) {
|
||||
guestOK = 1
|
||||
}
|
||||
result, err := d.conn.Exec(`
|
||||
INSERT INTO samba_shares (name, path, comment, read_only, guest_ok, valid_users, valid_groups)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
share.Name, share.Path, share.Comment, readOnly, guestOK, validUsers, validGroups,
|
||||
INSERT INTO samba_shares (name, path, comment, read_only, guest_ok, valid_users, valid_groups, invalid_users)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
share.Name, share.Path, share.Comment, readOnly, guestOK, validUsers, validGroups, invalidUsers,
|
||||
)
|
||||
if err != nil {
|
||||
return SambaShare{}, fmt.Errorf("create samba share: %w", err)
|
||||
@@ -243,6 +252,10 @@ func (d *DB) UpdateSambaShare(id int64, share SambaShare) (SambaShare, error) {
|
||||
if err != nil {
|
||||
return SambaShare{}, err
|
||||
}
|
||||
invalidUsers, err := encodeJSONStrings(share.InvalidUsers)
|
||||
if err != nil {
|
||||
return SambaShare{}, err
|
||||
}
|
||||
readOnly := 0
|
||||
if share.ReadOnly {
|
||||
readOnly = 1
|
||||
@@ -253,9 +266,9 @@ func (d *DB) UpdateSambaShare(id int64, share SambaShare) (SambaShare, error) {
|
||||
}
|
||||
result, err := d.conn.Exec(`
|
||||
UPDATE samba_shares
|
||||
SET name = ?, path = ?, comment = ?, read_only = ?, guest_ok = ?, valid_users = ?, valid_groups = ?, updated_at = datetime('now')
|
||||
SET name = ?, path = ?, comment = ?, read_only = ?, guest_ok = ?, valid_users = ?, valid_groups = ?, invalid_users = ?, updated_at = datetime('now')
|
||||
WHERE id = ?`,
|
||||
share.Name, share.Path, share.Comment, readOnly, guestOK, validUsers, validGroups, id,
|
||||
share.Name, share.Path, share.Comment, readOnly, guestOK, validUsers, validGroups, invalidUsers, id,
|
||||
)
|
||||
if err != nil {
|
||||
return SambaShare{}, fmt.Errorf("update samba share: %w", err)
|
||||
|
||||
@@ -24,6 +24,10 @@ func (d *DB) ReplaceSambaShares(shares []SambaShare) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invalidUsers, err := encodeJSONStrings(share.InvalidUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
readOnly := 0
|
||||
if share.ReadOnly {
|
||||
readOnly = 1
|
||||
@@ -33,9 +37,9 @@ func (d *DB) ReplaceSambaShares(shares []SambaShare) error {
|
||||
guestOK = 1
|
||||
}
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO samba_shares (name, path, comment, read_only, guest_ok, valid_users, valid_groups)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
share.Name, share.Path, share.Comment, readOnly, guestOK, validUsers, validGroups,
|
||||
INSERT INTO samba_shares (name, path, comment, read_only, guest_ok, valid_users, valid_groups, invalid_users)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
share.Name, share.Path, share.Comment, readOnly, guestOK, validUsers, validGroups, invalidUsers,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert samba share %s: %w", share.Name, err)
|
||||
|
||||
@@ -201,6 +201,35 @@ func (d *DB) ClearDeletedUser(username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) ReplaceUsers(users []User) error {
|
||||
tx, err := d.conn.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM system_users`); err != nil {
|
||||
return fmt.Errorf("clear system_users: %w", err)
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
groups, err := encodeJSONStrings(u.Groups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO system_users (username, groups, smb_enabled, disabled, pending_password)
|
||||
VALUES (?, ?, ?, ?, '')`,
|
||||
u.Username, groups, boolToInt(u.SMBEnabled), boolToInt(u.Disabled),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert user %s: %w", u.Username, err)
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (d *DB) GetAdminByUsername(username string) (Admin, error) {
|
||||
row := d.conn.QueryRow(
|
||||
`SELECT id, username, password_hash, created_at, updated_at FROM admins WHERE username = ?`,
|
||||
|
||||
@@ -8,34 +8,42 @@ import (
|
||||
"github.com/darroyo/nasctl/internal/db"
|
||||
"github.com/darroyo/nasctl/internal/modules/nfs"
|
||||
"github.com/darroyo/nasctl/internal/modules/samba"
|
||||
"github.com/darroyo/nasctl/internal/modules/users"
|
||||
)
|
||||
|
||||
type ImportResult struct {
|
||||
SambaImported int
|
||||
NFSImported int
|
||||
UsersImported int
|
||||
SambaSkipped bool
|
||||
NFSSkipped bool
|
||||
NFSSkipped bool
|
||||
UsersSkipped bool
|
||||
SambaError string
|
||||
NFSError string
|
||||
UsersError string
|
||||
}
|
||||
|
||||
type ImporterDB interface {
|
||||
ListSambaShares() ([]db.SambaShare, error)
|
||||
ListNFSExports() ([]db.NFSExport, error)
|
||||
ListUsers() ([]db.User, error)
|
||||
ReplaceSambaShares(shares []db.SambaShare) error
|
||||
ReplaceNFSExports(exports []db.NFSExport) error
|
||||
ReplaceUsers(users []db.User) error
|
||||
GetSetting(key string) (string, bool, error)
|
||||
SetSetting(key, value string) error
|
||||
MarkDirty(module string) error
|
||||
}
|
||||
|
||||
func ImportOnBoot(ctx context.Context, database ImporterDB, smbPath, exportsPath string) ImportResult {
|
||||
func ImportOnBoot(ctx context.Context, database ImporterDB, smbPath, exportsPath, adminUsername string) ImportResult {
|
||||
result := ImportResult{}
|
||||
|
||||
sambaDone, _, _ := database.GetSetting("import.samba.done")
|
||||
nfsDone, _, _ := database.GetSetting("import.nfs.done")
|
||||
usersDone, _, _ := database.GetSetting("import.users.done")
|
||||
|
||||
if sambaDone == "true" && nfsDone == "true" {
|
||||
allDone := sambaDone == "true" && nfsDone == "true" && usersDone == "true"
|
||||
if allDone {
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -59,8 +67,19 @@ func ImportOnBoot(ctx context.Context, database ImporterDB, smbPath, exportsPath
|
||||
}
|
||||
}
|
||||
|
||||
if result.SambaImported > 0 || result.NFSImported > 0 {
|
||||
log.Printf("[importer] imported %d samba shares, %d nfs exports", result.SambaImported, result.NFSImported)
|
||||
if usersDone != "true" {
|
||||
ur := importUsers(ctx, database, adminUsername)
|
||||
result.UsersImported = ur.count
|
||||
result.UsersSkipped = ur.skipped
|
||||
result.UsersError = ur.err
|
||||
if ur.err == "" {
|
||||
_ = database.SetSetting("import.users.done", "true")
|
||||
}
|
||||
}
|
||||
|
||||
if result.SambaImported > 0 || result.NFSImported > 0 || result.UsersImported > 0 {
|
||||
log.Printf("[importer] imported %d samba shares, %d nfs exports, %d users",
|
||||
result.SambaImported, result.NFSImported, result.UsersImported)
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -69,13 +88,14 @@ func ImportOnBoot(ctx context.Context, database ImporterDB, smbPath, exportsPath
|
||||
func ResetImportFlags(ctx context.Context, database ImporterDB) error {
|
||||
_ = database.SetSetting("import.samba.done", "")
|
||||
_ = database.SetSetting("import.nfs.done", "")
|
||||
_ = database.SetSetting("import.users.done", "")
|
||||
return nil
|
||||
}
|
||||
|
||||
type importStep struct {
|
||||
count int
|
||||
count int
|
||||
skipped bool
|
||||
err string
|
||||
err string
|
||||
}
|
||||
|
||||
func importSamba(ctx context.Context, database ImporterDB, path string) importStep {
|
||||
@@ -133,3 +153,31 @@ func importNFS(ctx context.Context, database ImporterDB, path string) importStep
|
||||
|
||||
return importStep{count: len(exports)}
|
||||
}
|
||||
|
||||
func importUsers(ctx context.Context, database ImporterDB, adminUsername string) importStep {
|
||||
imported, err := ImportSystemUsers(ctx, adminUsername)
|
||||
if err != nil {
|
||||
return importStep{err: err.Error()}
|
||||
}
|
||||
if imported == nil {
|
||||
return importStep{skipped: true}
|
||||
}
|
||||
|
||||
existing, err := database.ListUsers()
|
||||
if err != nil {
|
||||
return importStep{err: err.Error()}
|
||||
}
|
||||
if len(existing) > 0 {
|
||||
return importStep{skipped: true}
|
||||
}
|
||||
|
||||
if err := database.ReplaceUsers(imported); err != nil {
|
||||
return importStep{err: err.Error()}
|
||||
}
|
||||
|
||||
if err := database.MarkDirty(users.ModuleName); err != nil {
|
||||
return importStep{count: len(imported), err: fmt.Sprintf("imported but could not mark dirty: %v", err)}
|
||||
}
|
||||
|
||||
return importStep{count: len(imported)}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package importer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/darroyo/nasctl/internal/db"
|
||||
"github.com/darroyo/nasctl/internal/system"
|
||||
)
|
||||
|
||||
const (
|
||||
UserMinUID = 1000
|
||||
UserMaxUID = 60000
|
||||
)
|
||||
|
||||
var excludedUsernames = map[string]bool{
|
||||
"nobody": true,
|
||||
"nogroup": true,
|
||||
"sshd": true,
|
||||
"systemd": true,
|
||||
"messagebus": true,
|
||||
"polkitd": true,
|
||||
}
|
||||
|
||||
func ImportSystemUsers(ctx context.Context, adminUsername string) ([]db.User, error) {
|
||||
passwdMap, err := readPasswd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupMap, err := readGroups()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
smbUsers, err := listSMBUsers(ctx)
|
||||
if err != nil {
|
||||
smbUsers = map[string]bool{}
|
||||
}
|
||||
|
||||
var users []db.User
|
||||
for username, uid := range passwdMap {
|
||||
if uid < UserMinUID || uid > UserMaxUID {
|
||||
continue
|
||||
}
|
||||
if excludedUsernames[username] {
|
||||
continue
|
||||
}
|
||||
if username == adminUsername {
|
||||
continue
|
||||
}
|
||||
users = append(users, db.User{
|
||||
Username: username,
|
||||
Groups: groupMap[username],
|
||||
SMBEnabled: smbUsers[username],
|
||||
Disabled: false,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].Username < users[j].Username
|
||||
})
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func readPasswd() (map[string]int, error) {
|
||||
f, err := os.Open("/etc/passwd")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
result := make(map[string]int)
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
username := parts[0]
|
||||
uid, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result[username] = uid
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func readGroups() (map[string][]string, error) {
|
||||
f, err := os.Open("/etc/group")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
result := make(map[string][]string)
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
membersStr := strings.TrimSpace(parts[3])
|
||||
if membersStr == "" {
|
||||
continue
|
||||
}
|
||||
for _, member := range strings.Split(membersStr, ",") {
|
||||
member = strings.TrimSpace(member)
|
||||
if member != "" {
|
||||
result[member] = append(result[member], parts[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func listSMBUsers(ctx context.Context) (map[string]bool, error) {
|
||||
stdout, _, err := system.Run(ctx, "pdbedit", "-L")
|
||||
if err != nil {
|
||||
return map[string]bool{}, nil
|
||||
}
|
||||
result := make(map[string]bool)
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) < 1 {
|
||||
continue
|
||||
}
|
||||
result[parts[0]] = true
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -37,6 +37,7 @@ type templateShare struct {
|
||||
GuestOK bool
|
||||
ValidUsers []string
|
||||
ValidGroups []string
|
||||
InvalidUsers []string
|
||||
}
|
||||
|
||||
type templateData struct {
|
||||
@@ -109,13 +110,14 @@ func (m *Module) renderConfig(shares []db.SambaShare) ([]byte, error) {
|
||||
data := templateData{Shares: make([]templateShare, 0, len(shares))}
|
||||
for _, share := range shares {
|
||||
data.Shares = append(data.Shares, templateShare{
|
||||
Name: share.Name,
|
||||
Path: share.Path,
|
||||
Comment: share.Comment,
|
||||
ReadOnly: share.ReadOnly,
|
||||
GuestOK: share.GuestOK,
|
||||
ValidUsers: share.ValidUsers,
|
||||
ValidGroups: share.ValidGroups,
|
||||
Name: share.Name,
|
||||
Path: share.Path,
|
||||
Comment: share.Comment,
|
||||
ReadOnly: share.ReadOnly,
|
||||
GuestOK: share.GuestOK,
|
||||
ValidUsers: share.ValidUsers,
|
||||
ValidGroups: share.ValidGroups,
|
||||
InvalidUsers: share.InvalidUsers,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -20,5 +20,8 @@
|
||||
{{- if .ValidGroups}}
|
||||
valid groups = {{join .ValidGroups ","}}
|
||||
{{- end}}
|
||||
{{- if .InvalidUsers}}
|
||||
invalid users = {{join .InvalidUsers ","}}
|
||||
{{- end}}
|
||||
|
||||
{{end}}
|
||||
|
||||
@@ -196,6 +196,7 @@ type Server struct {
|
||||
Auth *AuthService
|
||||
SMBConfPath string
|
||||
ExportsPath string
|
||||
AdminUsername string
|
||||
UploadMaxBytes int64
|
||||
PreviewMaxBytes int64
|
||||
}
|
||||
@@ -205,6 +206,7 @@ type Options struct {
|
||||
Auth *AuthService
|
||||
SMBConfPath string
|
||||
ExportsPath string
|
||||
AdminUsername string
|
||||
UploadMaxBytes int64
|
||||
PreviewMaxBytes int64
|
||||
}
|
||||
@@ -223,6 +225,7 @@ func NewServer(database *db.DB, eng *engine.Engine, opts Options) *Server {
|
||||
Auth: opts.Auth,
|
||||
SMBConfPath: opts.SMBConfPath,
|
||||
ExportsPath: opts.ExportsPath,
|
||||
AdminUsername: opts.AdminUsername,
|
||||
UploadMaxBytes: opts.UploadMaxBytes,
|
||||
PreviewMaxBytes: opts.PreviewMaxBytes,
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"github.com/darroyo/nasctl/internal/importer"
|
||||
"github.com/darroyo/nasctl/internal/modules/nfs"
|
||||
"github.com/darroyo/nasctl/internal/modules/samba"
|
||||
"github.com/darroyo/nasctl/internal/modules/users"
|
||||
)
|
||||
|
||||
type importStatus struct {
|
||||
Samba moduleImportStatus `json:"samba"`
|
||||
NFS moduleImportStatus `json:"nfs"`
|
||||
Samba moduleImportStatus `json:"samba"`
|
||||
NFS moduleImportStatus `json:"nfs"`
|
||||
Users moduleImportStatus `json:"users"`
|
||||
}
|
||||
|
||||
type moduleImportStatus struct {
|
||||
@@ -24,9 +26,11 @@ type moduleImportStatus struct {
|
||||
func (s *Server) handleImportStatus(w http.ResponseWriter, r *http.Request) {
|
||||
sambaDone, _, _ := s.DB.GetSetting("import.samba.done")
|
||||
nfsDone, _, _ := s.DB.GetSetting("import.nfs.done")
|
||||
usersDone, _, _ := s.DB.GetSetting("import.users.done")
|
||||
|
||||
shares, _ := s.DB.ListSambaShares()
|
||||
exports, _ := s.DB.ListNFSExports()
|
||||
userList, _ := s.DB.ListUsers()
|
||||
|
||||
status := importStatus{
|
||||
Samba: moduleImportStatus{
|
||||
@@ -37,6 +41,10 @@ func (s *Server) handleImportStatus(w http.ResponseWriter, r *http.Request) {
|
||||
Done: nfsDone == "true",
|
||||
Count: len(exports),
|
||||
},
|
||||
Users: moduleImportStatus{
|
||||
Done: usersDone == "true",
|
||||
Count: len(userList),
|
||||
},
|
||||
}
|
||||
|
||||
if ts, ok, _ := s.DB.GetSetting("import.samba.at"); ok {
|
||||
@@ -45,6 +53,9 @@ func (s *Server) handleImportStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if ts, ok, _ := s.DB.GetSetting("import.nfs.at"); ok {
|
||||
status.NFS.LastImportAt = ts
|
||||
}
|
||||
if ts, ok, _ := s.DB.GetSetting("import.users.at"); ok {
|
||||
status.Users.LastImportAt = ts
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, status)
|
||||
}
|
||||
@@ -115,9 +126,43 @@ func (s *Server) handleImportNFS(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"imported": len(exports)})
|
||||
}
|
||||
|
||||
func (s *Server) handleImportUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if err := importer.ResetImportFlags(r.Context(), s.DB); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
imported, err := importer.ImportSystemUsers(r.Context(), s.AdminUsername)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(imported) == 0 {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"imported": 0, "message": "no system users found to import"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.DB.ReplaceUsers(imported); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.DB.MarkDirty(users.ModuleName); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.DB.SetSetting("import.users.done", "true")
|
||||
_ = s.DB.SetSetting("import.users.at", time.Now().Format(time.RFC3339))
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"imported": len(imported)})
|
||||
}
|
||||
|
||||
type dbExporter interface {
|
||||
ReplaceSambaShares(shares []db.SambaShare) error
|
||||
ReplaceNFSExports(exports []db.NFSExport) error
|
||||
ReplaceUsers(users []db.User) error
|
||||
MarkDirty(module string) error
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,14 @@ func writeError(w http.ResponseWriter, status int, message string) {
|
||||
}
|
||||
|
||||
type sambaShareRequest struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Comment string `json:"comment"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
GuestOK bool `json:"guest_ok"`
|
||||
ValidUsers []string `json:"valid_users"`
|
||||
ValidGroups []string `json:"valid_groups"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Comment string `json:"comment"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
GuestOK bool `json:"guest_ok"`
|
||||
ValidUsers []string `json:"valid_users"`
|
||||
ValidGroups []string `json:"valid_groups"`
|
||||
InvalidUsers []string `json:"invalid_users"`
|
||||
}
|
||||
|
||||
func (req sambaShareRequest) validate(allowedRoots []string) error {
|
||||
@@ -45,18 +46,24 @@ func (req sambaShareRequest) validate(allowedRoots []string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, user := range req.InvalidUsers {
|
||||
if err := system.ValidateUsername(user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req sambaShareRequest) toModel() db.SambaShare {
|
||||
return db.SambaShare{
|
||||
Name: req.Name,
|
||||
Path: req.Path,
|
||||
Comment: req.Comment,
|
||||
ReadOnly: req.ReadOnly,
|
||||
GuestOK: req.GuestOK,
|
||||
ValidUsers: req.ValidUsers,
|
||||
ValidGroups: req.ValidGroups,
|
||||
Name: req.Name,
|
||||
Path: req.Path,
|
||||
Comment: req.Comment,
|
||||
ReadOnly: req.ReadOnly,
|
||||
GuestOK: req.GuestOK,
|
||||
ValidUsers: req.ValidUsers,
|
||||
ValidGroups: req.ValidGroups,
|
||||
InvalidUsers: req.InvalidUsers,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ func NewRouter(s *Server) chi.Router {
|
||||
protected.Get("/import/status", s.handleImportStatus)
|
||||
protected.Post("/import/samba", s.handleImportSamba)
|
||||
protected.Post("/import/nfs", s.handleImportNFS)
|
||||
protected.Post("/import/users", s.handleImportUsers)
|
||||
|
||||
protected.Route("/samba/shares", func(shares chi.Router) {
|
||||
shares.Get("/", s.handleListSambaShares)
|
||||
|
||||
Reference in New Issue
Block a user