diff --git a/internal/api/handler/iot_device_handler.go b/internal/api/handler/iot_device_handler.go index 22dcd0e..285f92b 100644 --- a/internal/api/handler/iot_device_handler.go +++ b/internal/api/handler/iot_device_handler.go @@ -201,19 +201,33 @@ func (h *Handler) MQTTAuth(w http.ResponseWriter, r *http.Request) { // Продолжаем — это некритично, устройство всё равно авторизовано } - // Проверки пройдены — разрешаем подключение. - // ACL ограничивает устройство только его собственным топиком: - // publish: {namespace}/{deviceId} (данные устройства) - // subscribe: {namespace}/{deviceId} (команды устройству, если нужны) - // deny all: всё остальное запрещено — нельзя читать чужие данные - ownerTopic := ns + "/" + deviceID + "/#" + // Формируем ACL правила для этого подключения. + // Топик устройства: "{namespace}/telemetry/{deviceId}" + // Это то что строит эмулятор: topicPrefix + "telemetry/" + device_id + // topicPrefix = "{ns}/" → итого "{ns}/telemetry/{deviceId}" + deviceTopic := ns + "/telemetry/" + deviceID + + var aclRules []aclRule + + if req.ClientID == "sless-iot-bridge" { + // Bridge подписывается на "+/telemetry/+" (все тенанты) — разрешаем + // Bridge НЕ публикует через MQTT — только читает + aclRules = []aclRule{ + {Permission: "allow", Action: "subscribe", Topic: "+/telemetry/+"}, + {Permission: "deny", Action: "all", Topic: "#"}, + } + } else { + // Обычное IoT устройство: только свой топик + aclRules = []aclRule{ + {Permission: "allow", Action: "publish", Topic: deviceTopic}, + {Permission: "allow", Action: "subscribe", Topic: deviceTopic}, + {Permission: "deny", Action: "all", Topic: "#"}, + } + } + writeJSON(w, http.StatusOK, mqttAuthResponse{ Result: "allow", - ACL: []aclRule{ - {Permission: "allow", Action: "publish", Topic: ownerTopic}, - {Permission: "allow", Action: "subscribe", Topic: ownerTopic}, - {Permission: "deny", Action: "all", Topic: "#"}, - }, + ACL: aclRules, }) } @@ -392,8 +406,9 @@ type mqttAclRequest struct { // Вызывается EMQX для каждого pub/sub действия. // НЕ защищён JWT — доступен только из кластера. // -// Логика: клиент видит только топики вида {namespace}/{deviceId}/# -// Любой другой топик — deny и disconnect. +// Логика разрешений: +// 1. Bridge clientid "sless-iot-bridge" — subscribe на любой топик (нужен для "+/telemetry/+") +// 2. IoT Device (username "{ns}_{deviceId}") — publish/subscribe на "{ns}/telemetry/{deviceId}" func (h *Handler) MQTTAcl(w http.ResponseWriter, r *http.Request) { var req mqttAclRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -401,7 +416,19 @@ func (h *Handler) MQTTAcl(w http.ResponseWriter, r *http.Request) { return } + // Специальный случай: mqtt-bridge подписывается на "+/telemetry/+" (все тенанты). + // Публикация bridge НЕ разрешена — только чтение. + if req.ClientID == "sless-iot-bridge" { + if req.Action == "subscribe" { + writeJSON(w, http.StatusOK, mqttAuthResponse{Result: "allow"}) + } else { + writeJSON(w, http.StatusOK, mqttAuthResponse{Result: "deny"}) + } + return + } + // Парсим username → namespace + deviceId (формат: "{ns}_{deviceId}") + // strings.Index находит ПЕРВЫЙ '_' — namespace содержит только дефисы idx := strings.Index(req.Username, "_") if idx < 0 { writeJSON(w, http.StatusOK, mqttAuthResponse{Result: "deny"}) @@ -414,11 +441,11 @@ func (h *Handler) MQTTAcl(w http.ResponseWriter, r *http.Request) { return } - // Разрешаем только топики этого устройства: {ns}/{deviceId}/... - // Используем strings.HasPrefix — wildcard не нужен, проверяем prefix реального топика. - allowedPrefix := ns + "/" + deviceID + "/" - exactMatch := ns + "/" + deviceID - if strings.HasPrefix(req.Topic, allowedPrefix) || req.Topic == exactMatch { + // Разрешённый топик: "{ns}/telemetry/{deviceId}" + // Это то что эмулятор строит как: topicPrefix + "telemetry/" + device_id + // topicPrefix = "{ns}/" → итого "{ns}/telemetry/{deviceId}" + allowedTopic := ns + "/telemetry/" + deviceID + if req.Topic == allowedTopic { writeJSON(w, http.StatusOK, mqttAuthResponse{Result: "allow"}) return }