fix: MQTT ACL — allow bridge subscribe +/telemetry/+, fix device topic {ns}/telemetry/{deviceId}
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user