8003db49b3
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.
300 lines
7.4 KiB
Go
300 lines
7.4 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
func parseTime(value string) time.Time {
|
|
t, err := time.Parse(time.RFC3339, value)
|
|
if err != nil {
|
|
t, _ = time.Parse("2006-01-02 15:04:05", value)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func encodeJSONStrings(values []string) (string, error) {
|
|
if values == nil {
|
|
values = []string{}
|
|
}
|
|
data, err := json.Marshal(values)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func decodeJSONStrings(raw string) ([]string, error) {
|
|
if raw == "" {
|
|
return []string{}, nil
|
|
}
|
|
var values []string
|
|
if err := json.Unmarshal([]byte(raw), &values); err != nil {
|
|
return nil, err
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func scanSambaShare(row interface {
|
|
Scan(dest ...any) error
|
|
}) (SambaShare, error) {
|
|
var share SambaShare
|
|
var readOnly, guestOK int
|
|
var validUsers, validGroups, invalidUsers, createdAt, updatedAt string
|
|
if err := row.Scan(
|
|
&share.ID,
|
|
&share.Name,
|
|
&share.Path,
|
|
&share.Comment,
|
|
&readOnly,
|
|
&guestOK,
|
|
&validUsers,
|
|
&validGroups,
|
|
&invalidUsers,
|
|
&createdAt,
|
|
&updatedAt,
|
|
); err != nil {
|
|
return SambaShare{}, err
|
|
}
|
|
share.ReadOnly = readOnly != 0
|
|
share.GuestOK = guestOK != 0
|
|
var err error
|
|
share.ValidUsers, err = decodeJSONStrings(validUsers)
|
|
if err != nil {
|
|
return SambaShare{}, err
|
|
}
|
|
share.ValidGroups, err = decodeJSONStrings(validGroups)
|
|
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
|
|
}
|
|
|
|
func (d *DB) MarkDirty(module string) error {
|
|
_, err := d.conn.Exec(
|
|
`INSERT INTO dirty_modules (module, marked_at) VALUES (?, datetime('now'))
|
|
ON CONFLICT(module) DO UPDATE SET marked_at = datetime('now')`,
|
|
module,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("mark dirty %q: %w", module, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *DB) ClearDirty(module string) error {
|
|
_, err := d.conn.Exec(`DELETE FROM dirty_modules WHERE module = ?`, module)
|
|
if err != nil {
|
|
return fmt.Errorf("clear dirty %q: %w", module, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *DB) IsDirty(module string) (bool, error) {
|
|
var count int
|
|
err := d.conn.QueryRow(`SELECT COUNT(1) FROM dirty_modules WHERE module = ?`, module).Scan(&count)
|
|
if err != nil {
|
|
return false, fmt.Errorf("is dirty %q: %w", module, err)
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (d *DB) ListDirty() ([]DirtyModule, error) {
|
|
rows, err := d.conn.Query(`SELECT module, marked_at FROM dirty_modules ORDER BY marked_at ASC`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list dirty: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var modules []DirtyModule
|
|
for rows.Next() {
|
|
var module DirtyModule
|
|
var markedAt string
|
|
if err := rows.Scan(&module.Module, &markedAt); err != nil {
|
|
return nil, fmt.Errorf("scan dirty: %w", err)
|
|
}
|
|
module.MarkedAt = parseTime(markedAt)
|
|
modules = append(modules, module)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return modules, nil
|
|
}
|
|
|
|
func (d *DB) AppendApplyLog(module, message string, success bool) error {
|
|
successInt := 0
|
|
if success {
|
|
successInt = 1
|
|
}
|
|
_, err := d.conn.Exec(
|
|
`INSERT INTO apply_log (module, message, success, created_at) VALUES (?, ?, ?, datetime('now'))`,
|
|
module, message, successInt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("append apply log: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *DB) ListApplyLog(limit int) ([]ApplyLogEntry, error) {
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
rows, err := d.conn.Query(
|
|
`SELECT id, module, message, success, created_at FROM apply_log ORDER BY id DESC LIMIT ?`,
|
|
limit,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list apply log: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var entries []ApplyLogEntry
|
|
for rows.Next() {
|
|
var entry ApplyLogEntry
|
|
var success int
|
|
var createdAt string
|
|
if err := rows.Scan(&entry.ID, &entry.Module, &entry.Message, &success, &createdAt); err != nil {
|
|
return nil, fmt.Errorf("scan apply log: %w", err)
|
|
}
|
|
entry.Success = success != 0
|
|
entry.CreatedAt = parseTime(createdAt)
|
|
entries = append(entries, entry)
|
|
}
|
|
return entries, rows.Err()
|
|
}
|
|
|
|
func (d *DB) ListSambaShares() ([]SambaShare, error) {
|
|
rows, err := d.conn.Query(`
|
|
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)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var shares []SambaShare
|
|
for rows.Next() {
|
|
share, err := scanSambaShare(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan samba share: %w", err)
|
|
}
|
|
shares = append(shares, share)
|
|
}
|
|
return shares, rows.Err()
|
|
}
|
|
|
|
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, invalid_users, created_at, updated_at
|
|
FROM samba_shares WHERE id = ?`, id)
|
|
share, err := scanSambaShare(row)
|
|
if err == sql.ErrNoRows {
|
|
return SambaShare{}, fmt.Errorf("samba share not found")
|
|
}
|
|
if err != nil {
|
|
return SambaShare{}, fmt.Errorf("get samba share: %w", err)
|
|
}
|
|
return share, nil
|
|
}
|
|
|
|
func (d *DB) CreateSambaShare(share SambaShare) (SambaShare, error) {
|
|
validUsers, err := encodeJSONStrings(share.ValidUsers)
|
|
if err != nil {
|
|
return SambaShare{}, err
|
|
}
|
|
validGroups, err := encodeJSONStrings(share.ValidGroups)
|
|
if err != nil {
|
|
return SambaShare{}, err
|
|
}
|
|
invalidUsers, err := encodeJSONStrings(share.InvalidUsers)
|
|
if err != nil {
|
|
return SambaShare{}, err
|
|
}
|
|
readOnly := 0
|
|
if share.ReadOnly {
|
|
readOnly = 1
|
|
}
|
|
guestOK := 0
|
|
if share.GuestOK {
|
|
guestOK = 1
|
|
}
|
|
result, err := d.conn.Exec(`
|
|
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)
|
|
}
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return SambaShare{}, fmt.Errorf("last insert id: %w", err)
|
|
}
|
|
return d.GetSambaShare(id)
|
|
}
|
|
|
|
func (d *DB) UpdateSambaShare(id int64, share SambaShare) (SambaShare, error) {
|
|
validUsers, err := encodeJSONStrings(share.ValidUsers)
|
|
if err != nil {
|
|
return SambaShare{}, err
|
|
}
|
|
validGroups, err := encodeJSONStrings(share.ValidGroups)
|
|
if err != nil {
|
|
return SambaShare{}, err
|
|
}
|
|
invalidUsers, err := encodeJSONStrings(share.InvalidUsers)
|
|
if err != nil {
|
|
return SambaShare{}, err
|
|
}
|
|
readOnly := 0
|
|
if share.ReadOnly {
|
|
readOnly = 1
|
|
}
|
|
guestOK := 0
|
|
if share.GuestOK {
|
|
guestOK = 1
|
|
}
|
|
result, err := d.conn.Exec(`
|
|
UPDATE samba_shares
|
|
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, invalidUsers, id,
|
|
)
|
|
if err != nil {
|
|
return SambaShare{}, fmt.Errorf("update samba share: %w", err)
|
|
}
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return SambaShare{}, err
|
|
}
|
|
if rows == 0 {
|
|
return SambaShare{}, fmt.Errorf("samba share not found")
|
|
}
|
|
return d.GetSambaShare(id)
|
|
}
|
|
|
|
func (d *DB) DeleteSambaShare(id int64) error {
|
|
result, err := d.conn.Exec(`DELETE FROM samba_shares WHERE id = ?`, id)
|
|
if err != nil {
|
|
return fmt.Errorf("delete samba share: %w", err)
|
|
}
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rows == 0 {
|
|
return fmt.Errorf("samba share not found")
|
|
}
|
|
return nil
|
|
}
|