Enforce pricelist write checks and auto-restart on DB settings change
This commit is contained in:
25
README.md
25
README.md
@@ -87,27 +87,34 @@ go run ./cmd/qfs -migrate
|
|||||||
|
|
||||||
### Минимальные права БД для пользователя квотаций
|
### Минимальные права БД для пользователя квотаций
|
||||||
|
|
||||||
Если нужен пользователь, который может создавать/редактировать квотации, но не может управлять ценами:
|
Если нужен пользователь, который может работать с конфигурациями, но не может создавать/удалять прайслисты:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
DROP USER IF EXISTS 'quote_user'@'%';
|
-- 1) Создать (или оставить существующего) пользователя
|
||||||
CREATE USER 'quote_user'@'%' IDENTIFIED BY 'DB_PASSWORD_PLACEHOLDER';
|
CREATE USER IF NOT EXISTS 'quote_user'@'%' IDENTIFIED BY 'DB_PASSWORD_PLACEHOLDER';
|
||||||
|
|
||||||
-- чтение данных для расчета/просмотра
|
-- 2) Сбросить лишние права (без пересоздания пользователя)
|
||||||
|
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'quote_user'@'%';
|
||||||
|
|
||||||
|
-- 3) Чтение данных для конфигуратора и синка
|
||||||
GRANT SELECT ON RFQ_LOG.lot TO 'quote_user'@'%';
|
GRANT SELECT ON RFQ_LOG.lot TO 'quote_user'@'%';
|
||||||
GRANT SELECT ON RFQ_LOG.qt_lot_metadata TO 'quote_user'@'%';
|
GRANT SELECT ON RFQ_LOG.qt_lot_metadata TO 'quote_user'@'%';
|
||||||
|
GRANT SELECT ON RFQ_LOG.qt_categories TO 'quote_user'@'%';
|
||||||
GRANT SELECT ON RFQ_LOG.qt_pricelists TO 'quote_user'@'%';
|
GRANT SELECT ON RFQ_LOG.qt_pricelists TO 'quote_user'@'%';
|
||||||
GRANT SELECT ON RFQ_LOG.qt_pricelist_items TO 'quote_user'@'%';
|
GRANT SELECT ON RFQ_LOG.qt_pricelist_items TO 'quote_user'@'%';
|
||||||
GRANT SELECT ON RFQ_LOG.qt_users TO 'quote_user'@'%';
|
|
||||||
|
|
||||||
-- работа с квотациями
|
-- 4) Работа с конфигурациями
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON RFQ_LOG.qt_configurations TO 'quote_user'@'%';
|
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_configurations TO 'quote_user'@'%';
|
||||||
|
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
SHOW GRANTS FOR 'quote_user'@'%';
|
||||||
```
|
```
|
||||||
|
|
||||||
Важно: этот вариант не ограничивает редактирование только своими записями в `qt_configurations`.
|
Важно:
|
||||||
Если пересоздавать пользователя нельзя, используйте `SHOW GRANTS FOR 'quote_user'@'%';` и сделайте точечные `REVOKE`.
|
- не выдавайте `INSERT/UPDATE/DELETE` на `qt_pricelists` и `qt_pricelist_items`, если пользователь не должен управлять прайслистами;
|
||||||
|
- если используется host-специфичный аккаунт (`'quote_user'@'192.168.x.x'`), назначьте права и для него;
|
||||||
|
- после смены DB-настроек через `/setup` приложение перезапускается автоматически и подхватывает нового пользователя.
|
||||||
|
|
||||||
### 4. Импорт метаданных компонентов
|
### 4. Импорт метаданных компонентов
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(cfg.Server.Mode)
|
gin.SetMode(cfg.Server.Mode)
|
||||||
router, syncService, err := setupRouter(cfg, local, connMgr, mariaDB, dbUser)
|
restartSig := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
router, syncService, err := setupRouter(cfg, local, connMgr, mariaDB, dbUser, restartSig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to setup router", "error", err)
|
slog.Error("failed to setup router", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -207,9 +209,15 @@ func main() {
|
|||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-quit
|
|
||||||
|
|
||||||
slog.Info("shutting down server...")
|
shouldRestart := false
|
||||||
|
select {
|
||||||
|
case <-quit:
|
||||||
|
slog.Info("shutting down server...")
|
||||||
|
case <-restartSig:
|
||||||
|
shouldRestart = true
|
||||||
|
slog.Info("restarting application after connection settings update...")
|
||||||
|
}
|
||||||
|
|
||||||
// Stop background sync worker first
|
// Stop background sync worker first
|
||||||
syncWorker.Stop()
|
syncWorker.Stop()
|
||||||
@@ -224,6 +232,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("server stopped")
|
slog.Info("server stopped")
|
||||||
|
|
||||||
|
if shouldRestart {
|
||||||
|
restartProcess()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setConfigDefaults(cfg *config.Config) {
|
func setConfigDefaults(cfg *config.Config) {
|
||||||
@@ -394,7 +406,7 @@ func setupDatabaseFromDSN(dsn string) (*gorm.DB, error) {
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.ConnectionManager, mariaDB *gorm.DB, dbUsername string) (*gin.Engine, *sync.Service, error) {
|
func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.ConnectionManager, mariaDB *gorm.DB, dbUsername string, restartSig chan struct{}) (*gin.Engine, *sync.Service, error) {
|
||||||
// mariaDB may be nil if we're in offline mode
|
// mariaDB may be nil if we're in offline mode
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
@@ -469,8 +481,8 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
|
|||||||
return nil, nil, fmt.Errorf("creating sync handler: %w", err)
|
return nil, nil, fmt.Errorf("creating sync handler: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup handler (for reconfiguration) - no restart signal in normal mode
|
// Setup handler (for reconfiguration)
|
||||||
setupHandler, err := handlers.NewSetupHandler(local, connMgr, templatesPath, nil)
|
setupHandler, err := handlers.NewSetupHandler(local, connMgr, templatesPath, restartSig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("creating setup handler: %w", err)
|
return nil, nil, fmt.Errorf("creating setup handler: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func TestConfigurationVersioningAPI(t *testing.T) {
|
|||||||
|
|
||||||
cfg := &config.Config{}
|
cfg := &config.Config{}
|
||||||
setConfigDefaults(cfg)
|
setConfigDefaults(cfg)
|
||||||
router, _, err := setupRouter(cfg, local, connMgr, nil, "tester")
|
router, _, err := setupRouter(cfg, local, connMgr, nil, "tester", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("setup router: %v", err)
|
t.Fatalf("setup router: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ func (h *PricelistHandler) Get(c *gin.Context) {
|
|||||||
|
|
||||||
// Create creates a new pricelist from current prices
|
// Create creates a new pricelist from current prices
|
||||||
func (h *PricelistHandler) Create(c *gin.Context) {
|
func (h *PricelistHandler) Create(c *gin.Context) {
|
||||||
|
canWrite, debugInfo := h.service.CanWriteDebug()
|
||||||
|
if !canWrite {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"error": "pricelist write is not allowed",
|
||||||
|
"debug": debugInfo,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get the database username as the creator
|
// Get the database username as the creator
|
||||||
createdBy := h.localDB.GetDBUser()
|
createdBy := h.localDB.GetDBUser()
|
||||||
if createdBy == "" {
|
if createdBy == "" {
|
||||||
@@ -104,6 +113,15 @@ func (h *PricelistHandler) Create(c *gin.Context) {
|
|||||||
|
|
||||||
// Delete deletes a pricelist by ID
|
// Delete deletes a pricelist by ID
|
||||||
func (h *PricelistHandler) Delete(c *gin.Context) {
|
func (h *PricelistHandler) Delete(c *gin.Context) {
|
||||||
|
canWrite, debugInfo := h.service.CanWriteDebug()
|
||||||
|
if !canWrite {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"error": "pricelist write is not allowed",
|
||||||
|
"debug": debugInfo,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ func (h *SetupHandler) TestConnection(c *gin.Context) {
|
|||||||
|
|
||||||
// SaveConnection saves the connection settings and signals restart
|
// SaveConnection saves the connection settings and signals restart
|
||||||
func (h *SetupHandler) SaveConnection(c *gin.Context) {
|
func (h *SetupHandler) SaveConnection(c *gin.Context) {
|
||||||
|
existingSettings, _ := h.localDB.GetSettings()
|
||||||
|
|
||||||
host := c.PostForm("host")
|
host := c.PostForm("host")
|
||||||
portStr := c.PostForm("port")
|
portStr := c.PostForm("port")
|
||||||
database := c.PostForm("database")
|
database := c.PostForm("database")
|
||||||
@@ -202,19 +204,29 @@ func (h *SetupHandler) SaveConnection(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always restart to properly initialize all services with the new connection
|
settingsChanged := existingSettings == nil ||
|
||||||
restartRequired := h.restartSig == nil
|
existingSettings.Host != host ||
|
||||||
|
existingSettings.Port != port ||
|
||||||
|
existingSettings.Database != database ||
|
||||||
|
existingSettings.User != user ||
|
||||||
|
existingSettings.PasswordEncrypted != password
|
||||||
|
|
||||||
|
restartQueued := settingsChanged && h.restartSig != nil
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Settings saved.",
|
"message": "Settings saved.",
|
||||||
"restart_required": restartRequired,
|
"restart_required": settingsChanged,
|
||||||
|
"restart_queued": restartQueued,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Signal restart after response is sent (if restart signal is configured)
|
// Signal restart after response is sent (if restart signal is configured)
|
||||||
if h.restartSig != nil {
|
if restartQueued {
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(500 * time.Millisecond) // Give time for response to be sent
|
time.Sleep(500 * time.Millisecond) // Give time for response to be sent
|
||||||
h.restartSig <- struct{}{}
|
select {
|
||||||
|
case h.restartSig <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user