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
|
||||
DROP USER IF EXISTS 'quote_user'@'%';
|
||||
CREATE USER 'quote_user'@'%' IDENTIFIED BY 'DB_PASSWORD_PLACEHOLDER';
|
||||
-- 1) Создать (или оставить существующего) пользователя
|
||||
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.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_pricelist_items TO 'quote_user'@'%';
|
||||
GRANT SELECT ON RFQ_LOG.qt_users TO 'quote_user'@'%';
|
||||
|
||||
-- работа с квотациями
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON RFQ_LOG.qt_configurations TO 'quote_user'@'%';
|
||||
-- 4) Работа с конфигурациями
|
||||
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_configurations TO 'quote_user'@'%';
|
||||
|
||||
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. Импорт метаданных компонентов
|
||||
|
||||
|
||||
@@ -165,7 +165,9 @@ func main() {
|
||||
}
|
||||
|
||||
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 {
|
||||
slog.Error("failed to setup router", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -207,9 +209,15 @@ func main() {
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
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
|
||||
syncWorker.Stop()
|
||||
@@ -224,6 +232,10 @@ func main() {
|
||||
}
|
||||
|
||||
slog.Info("server stopped")
|
||||
|
||||
if shouldRestart {
|
||||
restartProcess()
|
||||
}
|
||||
}
|
||||
|
||||
func setConfigDefaults(cfg *config.Config) {
|
||||
@@ -394,7 +406,7 @@ func setupDatabaseFromDSN(dsn string) (*gorm.DB, error) {
|
||||
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
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Setup handler (for reconfiguration) - no restart signal in normal mode
|
||||
setupHandler, err := handlers.NewSetupHandler(local, connMgr, templatesPath, nil)
|
||||
// Setup handler (for reconfiguration)
|
||||
setupHandler, err := handlers.NewSetupHandler(local, connMgr, templatesPath, restartSig)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("creating setup handler: %w", err)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestConfigurationVersioningAPI(t *testing.T) {
|
||||
|
||||
cfg := &config.Config{}
|
||||
setConfigDefaults(cfg)
|
||||
router, _, err := setupRouter(cfg, local, connMgr, nil, "tester")
|
||||
router, _, err := setupRouter(cfg, local, connMgr, nil, "tester", nil)
|
||||
if err != nil {
|
||||
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
|
||||
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
|
||||
createdBy := h.localDB.GetDBUser()
|
||||
if createdBy == "" {
|
||||
@@ -104,6 +113,15 @@ func (h *PricelistHandler) Create(c *gin.Context) {
|
||||
|
||||
// Delete deletes a pricelist by ID
|
||||
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")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
|
||||
@@ -148,6 +148,8 @@ func (h *SetupHandler) TestConnection(c *gin.Context) {
|
||||
|
||||
// SaveConnection saves the connection settings and signals restart
|
||||
func (h *SetupHandler) SaveConnection(c *gin.Context) {
|
||||
existingSettings, _ := h.localDB.GetSettings()
|
||||
|
||||
host := c.PostForm("host")
|
||||
portStr := c.PostForm("port")
|
||||
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
|
||||
restartRequired := h.restartSig == nil
|
||||
settingsChanged := existingSettings == 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{
|
||||
"success": true,
|
||||
"message": "Settings saved.",
|
||||
"restart_required": restartRequired,
|
||||
"restart_required": settingsChanged,
|
||||
"restart_queued": restartQueued,
|
||||
})
|
||||
|
||||
// Signal restart after response is sent (if restart signal is configured)
|
||||
if h.restartSig != nil {
|
||||
if restartQueued {
|
||||
go func() {
|
||||
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