1822 lines
44 KiB
Go
1822 lines
44 KiB
Go
// backend\operations_milestone_setup.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type operationMilestoneSetupRequest struct {
|
|
CameraDeviceID string `json:"cameraDeviceId"`
|
|
RouterDeviceID string `json:"routerDeviceId"`
|
|
SwitchboxDeviceID string `json:"switchboxDeviceId"`
|
|
HardDriveDeviceID string `json:"hardDriveDeviceId"`
|
|
LaptopDeviceID string `json:"laptopDeviceId"`
|
|
MilestoneUserID string `json:"milestoneUserId"`
|
|
MilestoneUserName string `json:"milestoneUserName"`
|
|
MilestoneUserSID string `json:"milestoneUserSid"`
|
|
MilestoneUserDN string `json:"milestoneUserDn"`
|
|
}
|
|
|
|
type operationMilestoneSetupEvent struct {
|
|
Type string `json:"type"`
|
|
Step string `json:"step,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type operationMilestoneSetupDevice struct {
|
|
ID string
|
|
InventoryNumber string
|
|
Manufacturer string
|
|
Model string
|
|
IPAddress string
|
|
PhoneNumber string
|
|
ComputerName string
|
|
DeviceCategory string
|
|
MilestoneHardwareID string
|
|
MilestoneDisplayName string
|
|
MilestonePort string
|
|
}
|
|
|
|
type operationMilestoneRole struct {
|
|
ID string
|
|
Name string
|
|
DisplayName string
|
|
Created bool
|
|
Data map[string]any
|
|
}
|
|
|
|
var operationMilestoneNumberPattern = regexp.MustCompile(`\d+`)
|
|
var operationMilestoneDetailSuffixPattern = regexp.MustCompile(`\s*\([^)]*\)\s*$`)
|
|
var operationMilestoneEvaluationRolePattern = regexp.MustCompile(`(?i)\bauswertung\s*([1-9]|10)\b`)
|
|
|
|
func (s *Server) handleOperationMilestoneSetup(w http.ResponseWriter, r *http.Request) {
|
|
operationID := strings.TrimSpace(r.PathValue("id"))
|
|
if operationID == "" {
|
|
writeError(w, http.StatusBadRequest, "Einsatz-ID fehlt")
|
|
return
|
|
}
|
|
|
|
var input operationMilestoneSetupRequest
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/x-ndjson")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
|
|
report := func(event operationMilestoneSetupEvent) {
|
|
writeOperationMilestoneSetupEvent(w, event)
|
|
}
|
|
fail := func(step string, message string, err error) {
|
|
if err != nil {
|
|
logError("Milestone-Nachlauf fehlgeschlagen", err, LogFields{
|
|
"operationId": operationID,
|
|
"step": step,
|
|
})
|
|
}
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "error",
|
|
Step: step,
|
|
Status: "error",
|
|
Message: message,
|
|
})
|
|
}
|
|
|
|
ctx := r.Context()
|
|
|
|
operation, err := s.getOperationForMilestoneSetup(ctx, operationID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
fail("viewGroup", "Einsatz wurde nicht gefunden", err)
|
|
return
|
|
}
|
|
if err != nil {
|
|
fail("viewGroup", "Einsatz konnte nicht geladen werden", err)
|
|
return
|
|
}
|
|
|
|
config, err := s.getMilestoneRuntimeConfig(ctx)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
fail("viewGroup", "Milestone-Einstellungen sind noch nicht hinterlegt", err)
|
|
return
|
|
}
|
|
if err != nil {
|
|
fail("viewGroup", "Milestone-Token konnte nicht geladen oder erneuert werden", err)
|
|
return
|
|
}
|
|
if strings.TrimSpace(config.Host) == "" {
|
|
fail("viewGroup", "Milestone Host fehlt", nil)
|
|
return
|
|
}
|
|
if strings.TrimSpace(config.AccessToken) == "" {
|
|
fail("viewGroup", "Milestone-Token fehlt und konnte nicht automatisch erneuert werden", nil)
|
|
return
|
|
}
|
|
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "viewGroup",
|
|
Status: "current",
|
|
Message: "Ansichtsgruppe wird gesucht oder angelegt.",
|
|
})
|
|
viewGroupID, err := s.ensureOperationMilestoneViewGroup(ctx, config, operation)
|
|
if err != nil {
|
|
fail("viewGroup", "Ansichtsgruppe konnte in Milestone nicht angelegt werden", err)
|
|
return
|
|
}
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "viewGroup",
|
|
Status: "complete",
|
|
Message: fmt.Sprintf("Ansichtsgruppe %q ist bereit.", operationMilestoneViewGroupName(operation)),
|
|
})
|
|
|
|
var cameraDevice *operationMilestoneSetupDevice
|
|
var routerDevice *operationMilestoneSetupDevice
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "cameraIp",
|
|
Status: "current",
|
|
Message: "Kamera-IP wird berechnet.",
|
|
})
|
|
cameraDevice, routerDevice, err = s.updateOperationMilestoneCameraIP(ctx, config, operation, input)
|
|
if err != nil {
|
|
fail("cameraIp", "Kamera-IP konnte nicht gesetzt werden", err)
|
|
return
|
|
}
|
|
if cameraDevice == nil || routerDevice == nil {
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "cameraIp",
|
|
Status: "complete",
|
|
Message: "Keine Kamera oder kein Router ausgewaehlt, Schritt uebersprungen.",
|
|
})
|
|
} else {
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "cameraIp",
|
|
Status: "complete",
|
|
Message: fmt.Sprintf("Kamera-IP wurde auf %s gesetzt.", cameraDevice.IPAddress),
|
|
})
|
|
}
|
|
|
|
var targetRole *operationMilestoneRole
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "roleUser",
|
|
Status: "current",
|
|
Message: "Einsatzrolle und Auswerter-Benutzer werden geprueft.",
|
|
})
|
|
targetRole, err = s.ensureOperationMilestoneRoleUser(ctx, config, operation, input)
|
|
if err != nil {
|
|
fail("roleUser", "AD-Benutzer konnte der Rolle nicht zugeordnet werden", err)
|
|
return
|
|
}
|
|
if targetRole == nil {
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "roleUser",
|
|
Status: "complete",
|
|
Message: "Kein Auswerter-Laptop ausgewaehlt, Schritt uebersprungen.",
|
|
})
|
|
} else if targetRole.Created {
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "roleUser",
|
|
Status: "complete",
|
|
Message: fmt.Sprintf("Rolle %q wurde angelegt.", operationMilestoneRoleDisplayName(*targetRole)),
|
|
})
|
|
} else {
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "roleUser",
|
|
Status: "complete",
|
|
Message: fmt.Sprintf("Rolle %q ist vorbereitet.", operationMilestoneRoleDisplayName(*targetRole)),
|
|
})
|
|
}
|
|
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "roleCamera",
|
|
Status: "current",
|
|
Message: "Kamera wird der Rolle zugeordnet.",
|
|
})
|
|
if targetRole == nil || cameraDevice == nil {
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "roleCamera",
|
|
Status: "complete",
|
|
Message: "Keine Rolle oder Kamera verfuegbar, Schritt uebersprungen.",
|
|
})
|
|
} else if message, err := s.assignOperationMilestoneCameraToRole(ctx, config, *targetRole, *cameraDevice); err != nil {
|
|
fail("roleCamera", "Kamera konnte der Rolle nicht zugeordnet werden", err)
|
|
return
|
|
} else {
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "roleCamera",
|
|
Status: "complete",
|
|
Message: message,
|
|
})
|
|
}
|
|
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "roleViewGroup",
|
|
Status: "current",
|
|
Message: "Ansichtsgruppe wird der Rolle zugeordnet.",
|
|
})
|
|
if targetRole == nil || viewGroupID == "" {
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "roleViewGroup",
|
|
Status: "complete",
|
|
Message: "Keine Rolle oder Ansichtsgruppe verfuegbar, Schritt uebersprungen.",
|
|
})
|
|
} else if message, err := s.assignOperationMilestoneViewGroupToRole(ctx, config, *targetRole, viewGroupID); err != nil {
|
|
fail("roleViewGroup", "Ansichtsgruppe konnte der Rolle nicht zugeordnet werden", err)
|
|
return
|
|
} else {
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "roleViewGroup",
|
|
Status: "complete",
|
|
Message: message,
|
|
})
|
|
}
|
|
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "otherRoles",
|
|
Status: "current",
|
|
Message: "Weitere Rollen werden geprueft.",
|
|
})
|
|
otherRoleCount, err := s.countOperationMilestoneEvaluationRoles(ctx, config)
|
|
if err != nil {
|
|
fail("otherRoles", "Andere Rollen konnten nicht geprueft werden", err)
|
|
return
|
|
}
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "step",
|
|
Step: "otherRoles",
|
|
Status: "complete",
|
|
Message: fmt.Sprintf("%d Auswertungsrollen wurden gefunden.", otherRoleCount),
|
|
})
|
|
|
|
report(operationMilestoneSetupEvent{
|
|
Type: "done",
|
|
Message: "Milestone-Nachlauf abgeschlossen.",
|
|
})
|
|
}
|
|
|
|
func writeOperationMilestoneSetupEvent(w http.ResponseWriter, event operationMilestoneSetupEvent) {
|
|
if err := json.NewEncoder(w).Encode(event); err != nil {
|
|
logError("Milestone-Nachlauf-Event konnte nicht geschrieben werden", err, nil)
|
|
return
|
|
}
|
|
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
|
|
func (s *Server) getOperationForMilestoneSetup(ctx context.Context, operationID string) (Operation, error) {
|
|
var operation Operation
|
|
|
|
err := s.db.QueryRow(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
id,
|
|
operation_number,
|
|
operation_name,
|
|
offense,
|
|
case_worker,
|
|
target_object,
|
|
kw_address,
|
|
kw_latitude,
|
|
kw_longitude,
|
|
target_latitude,
|
|
target_longitude,
|
|
view_cone_fov,
|
|
view_cone_length,
|
|
operation_leader,
|
|
operation_team,
|
|
legend,
|
|
camera,
|
|
router,
|
|
technology,
|
|
switchbox,
|
|
hard_drive,
|
|
laptop,
|
|
remark,
|
|
milestone_storage,
|
|
completed_at,
|
|
created_at,
|
|
updated_at
|
|
FROM operations
|
|
WHERE id = $1
|
|
LIMIT 1
|
|
`,
|
|
operationID,
|
|
).Scan(
|
|
&operation.ID,
|
|
&operation.OperationNumber,
|
|
&operation.OperationName,
|
|
&operation.Offense,
|
|
&operation.CaseWorker,
|
|
&operation.TargetObject,
|
|
&operation.KwAddress,
|
|
&operation.KwLatitude,
|
|
&operation.KwLongitude,
|
|
&operation.TargetLatitude,
|
|
&operation.TargetLongitude,
|
|
&operation.ViewConeFOV,
|
|
&operation.ViewConeLength,
|
|
&operation.OperationLeader,
|
|
&operation.OperationTeam,
|
|
&operation.Legend,
|
|
&operation.Camera,
|
|
&operation.Router,
|
|
&operation.Technology,
|
|
&operation.Switchbox,
|
|
&operation.HardDrive,
|
|
&operation.Laptop,
|
|
&operation.Remark,
|
|
&operation.MilestoneStorage,
|
|
&operation.CompletedAt,
|
|
&operation.CreatedAt,
|
|
&operation.UpdatedAt,
|
|
)
|
|
|
|
return operation, err
|
|
}
|
|
|
|
func (s *Server) getOperationMilestoneSetupDevice(
|
|
ctx context.Context,
|
|
deviceID string,
|
|
displayValue string,
|
|
category string,
|
|
) (*operationMilestoneSetupDevice, error) {
|
|
deviceID = strings.TrimSpace(deviceID)
|
|
if deviceID != "" {
|
|
device, err := s.getOperationMilestoneSetupDeviceByID(ctx, deviceID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &device, nil
|
|
}
|
|
|
|
displayValue = normalizeOperationMilestoneEquipmentDisplayValue(displayValue)
|
|
if displayValue == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
device, err := s.getOperationMilestoneSetupDeviceByDisplayValue(ctx, displayValue, category)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &device, nil
|
|
}
|
|
|
|
func (s *Server) getOperationMilestoneSetupDeviceByID(
|
|
ctx context.Context,
|
|
deviceID string,
|
|
) (operationMilestoneSetupDevice, error) {
|
|
var device operationMilestoneSetupDevice
|
|
|
|
err := s.db.QueryRow(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
id::TEXT,
|
|
inventory_number,
|
|
manufacturer,
|
|
model,
|
|
COALESCE(ip_address, ''),
|
|
COALESCE(phone_number, ''),
|
|
COALESCE(computer_name, ''),
|
|
COALESCE(device_category, ''),
|
|
COALESCE(milestone_hardware_id, ''),
|
|
COALESCE(milestone_display_name, ''),
|
|
COALESCE(milestone_port, '')
|
|
FROM devices
|
|
WHERE id = $1
|
|
LIMIT 1
|
|
`,
|
|
deviceID,
|
|
).Scan(
|
|
&device.ID,
|
|
&device.InventoryNumber,
|
|
&device.Manufacturer,
|
|
&device.Model,
|
|
&device.IPAddress,
|
|
&device.PhoneNumber,
|
|
&device.ComputerName,
|
|
&device.DeviceCategory,
|
|
&device.MilestoneHardwareID,
|
|
&device.MilestoneDisplayName,
|
|
&device.MilestonePort,
|
|
)
|
|
|
|
return device, err
|
|
}
|
|
|
|
func (s *Server) getOperationMilestoneSetupDeviceByDisplayValue(
|
|
ctx context.Context,
|
|
displayValue string,
|
|
category string,
|
|
) (operationMilestoneSetupDevice, error) {
|
|
var device operationMilestoneSetupDevice
|
|
|
|
err := s.db.QueryRow(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
id::TEXT,
|
|
inventory_number,
|
|
manufacturer,
|
|
model,
|
|
COALESCE(ip_address, ''),
|
|
COALESCE(phone_number, ''),
|
|
COALESCE(computer_name, ''),
|
|
COALESCE(device_category, ''),
|
|
COALESCE(milestone_hardware_id, ''),
|
|
COALESCE(milestone_display_name, ''),
|
|
COALESCE(milestone_port, '')
|
|
FROM devices
|
|
WHERE lower(COALESCE(device_category, '')) = lower($2)
|
|
AND (
|
|
inventory_number = $1
|
|
OR model = $1
|
|
OR COALESCE(milestone_display_name, '') = $1
|
|
OR COALESCE(computer_name, '') = $1
|
|
OR COALESCE(ip_address, '') = $1
|
|
)
|
|
ORDER BY updated_at DESC
|
|
LIMIT 1
|
|
`,
|
|
displayValue,
|
|
category,
|
|
).Scan(
|
|
&device.ID,
|
|
&device.InventoryNumber,
|
|
&device.Manufacturer,
|
|
&device.Model,
|
|
&device.IPAddress,
|
|
&device.PhoneNumber,
|
|
&device.ComputerName,
|
|
&device.DeviceCategory,
|
|
&device.MilestoneHardwareID,
|
|
&device.MilestoneDisplayName,
|
|
&device.MilestonePort,
|
|
)
|
|
|
|
return device, err
|
|
}
|
|
|
|
func normalizeOperationMilestoneEquipmentDisplayValue(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
for operationMilestoneDetailSuffixPattern.MatchString(value) {
|
|
value = strings.TrimSpace(operationMilestoneDetailSuffixPattern.ReplaceAllString(value, ""))
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func (s *Server) updateOperationMilestoneCameraIP(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
operation Operation,
|
|
input operationMilestoneSetupRequest,
|
|
) (*operationMilestoneSetupDevice, *operationMilestoneSetupDevice, error) {
|
|
cameraDevice, err := s.getOperationMilestoneSetupDevice(
|
|
ctx,
|
|
input.CameraDeviceID,
|
|
operation.Camera,
|
|
"Kameras",
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
routerDevice, err := s.getOperationMilestoneSetupDevice(
|
|
ctx,
|
|
input.RouterDeviceID,
|
|
operation.Router,
|
|
"Router",
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if cameraDevice == nil || routerDevice == nil {
|
|
return cameraDevice, routerDevice, nil
|
|
}
|
|
|
|
if strings.TrimSpace(cameraDevice.MilestoneHardwareID) == "" {
|
|
return cameraDevice, routerDevice, fmt.Errorf("Kamera %q ist kein Milestone-Hardwaregeraet", cameraDevice.InventoryNumber)
|
|
}
|
|
|
|
nextIP, err := operationMilestoneCameraIPAddress(*routerDevice, *cameraDevice)
|
|
if err != nil {
|
|
return cameraDevice, routerDevice, err
|
|
}
|
|
|
|
nextAddress := normalizeMilestoneHardwareAddress(nextIP, cameraDevice.MilestonePort)
|
|
if err := updateMilestoneHardware(
|
|
ctx,
|
|
config,
|
|
cameraDevice.MilestoneHardwareID,
|
|
map[string]any{"address": nextAddress},
|
|
cameraDevice.MilestoneDisplayName,
|
|
cameraDevice.MilestoneDisplayName,
|
|
); err != nil {
|
|
return cameraDevice, routerDevice, err
|
|
}
|
|
|
|
if _, err := s.db.Exec(
|
|
ctx,
|
|
`
|
|
UPDATE devices
|
|
SET
|
|
ip_address = $2,
|
|
milestone_synced_at = now(),
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
`,
|
|
cameraDevice.ID,
|
|
nextIP,
|
|
); err != nil {
|
|
return cameraDevice, routerDevice, err
|
|
}
|
|
|
|
cameraDevice.IPAddress = nextIP
|
|
|
|
return cameraDevice, routerDevice, nil
|
|
}
|
|
|
|
func operationMilestoneCameraIPAddress(
|
|
routerDevice operationMilestoneSetupDevice,
|
|
cameraDevice operationMilestoneSetupDevice,
|
|
) (string, error) {
|
|
routerIP := net.ParseIP(strings.TrimSpace(routerDevice.IPAddress)).To4()
|
|
if routerIP == nil {
|
|
return "", fmt.Errorf("Router %q hat keine gueltige IPv4-Adresse", routerDevice.InventoryNumber)
|
|
}
|
|
|
|
cameraNumber, err := operationMilestoneCameraNumber(cameraDevice)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return fmt.Sprintf(
|
|
"%d.%d.%d.%d",
|
|
routerIP[0],
|
|
routerIP[1],
|
|
routerIP[2],
|
|
cameraNumber,
|
|
), nil
|
|
}
|
|
|
|
func operationMilestoneCameraNumber(device operationMilestoneSetupDevice) (int, error) {
|
|
candidates := []string{
|
|
device.InventoryNumber,
|
|
device.MilestoneDisplayName,
|
|
device.Model,
|
|
}
|
|
|
|
for _, candidate := range candidates {
|
|
matches := operationMilestoneNumberPattern.FindAllString(candidate, -1)
|
|
if len(matches) == 0 {
|
|
continue
|
|
}
|
|
|
|
value, err := strconv.Atoi(matches[len(matches)-1])
|
|
if err == nil && value > 0 && value < 255 {
|
|
return value, nil
|
|
}
|
|
}
|
|
|
|
return 0, fmt.Errorf("Kameranummer konnte fuer %q nicht ermittelt werden", device.InventoryNumber)
|
|
}
|
|
|
|
func operationMilestoneViewGroupName(operation Operation) string {
|
|
operationNumber := operationMilestoneRoleName(operation)
|
|
operationName := strings.TrimSpace(operation.OperationName)
|
|
if operationName == "" {
|
|
return operationNumber
|
|
}
|
|
if operationNumber == "" {
|
|
return operationName
|
|
}
|
|
|
|
return operationNumber + " - " + operationName
|
|
}
|
|
|
|
func operationMilestoneRoleName(operation Operation) string {
|
|
return strings.ReplaceAll(strings.TrimSpace(operation.OperationNumber), "-", "")
|
|
}
|
|
|
|
func (s *Server) ensureOperationMilestoneViewGroup(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
operation Operation,
|
|
) (string, error) {
|
|
viewGroupName := operationMilestoneViewGroupName(operation)
|
|
if viewGroupName == "" {
|
|
return "", errors.New("Einsatznummer oder Einsatzname fehlt")
|
|
}
|
|
|
|
viewGroups, err := listOperationMilestoneViewGroups(ctx, config)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if existingID := findOperationMilestoneItemIDByName(viewGroups, viewGroupName); existingID != "" {
|
|
return existingID, nil
|
|
}
|
|
|
|
parentID := operationMilestoneViewGroupParentID(viewGroups)
|
|
payloads := []map[string]any{
|
|
{
|
|
"name": viewGroupName,
|
|
"displayName": viewGroupName,
|
|
},
|
|
}
|
|
if parentID != "" {
|
|
payloads = append([]map[string]any{
|
|
{
|
|
"name": viewGroupName,
|
|
"displayName": viewGroupName,
|
|
"parentId": parentID,
|
|
},
|
|
}, payloads...)
|
|
}
|
|
|
|
var lastErr error
|
|
for _, payload := range payloads {
|
|
body, _, err := operationMilestoneRequestJSON(
|
|
ctx,
|
|
config,
|
|
http.MethodPost,
|
|
"/API/rest/v1/viewGroups",
|
|
payload,
|
|
)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
if id := milestoneItemID(decodeOperationMilestoneObject(body)); id != "" {
|
|
return id, nil
|
|
}
|
|
|
|
viewGroups, err = listOperationMilestoneViewGroups(ctx, config)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if id := findOperationMilestoneItemIDByName(viewGroups, viewGroupName); id != "" {
|
|
return id, nil
|
|
}
|
|
}
|
|
|
|
if lastErr != nil {
|
|
return "", lastErr
|
|
}
|
|
|
|
return "", fmt.Errorf("Milestone hat keine ID fuer Ansichtsgruppe %q zurueckgegeben", viewGroupName)
|
|
}
|
|
|
|
func listOperationMilestoneViewGroups(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
) ([]map[string]any, error) {
|
|
collected := []map[string]any{}
|
|
visited := map[string]bool{}
|
|
|
|
topLevel, err := listOperationMilestoneObjectsAtPath(ctx, config, "/API/rest/v1/viewGroups")
|
|
if err == nil {
|
|
collected = append(collected, topLevel...)
|
|
for _, item := range topLevel {
|
|
collectOperationMilestoneViewGroupChildren(ctx, config, item, visited, &collected)
|
|
}
|
|
}
|
|
|
|
rootCandidates := []string{
|
|
strings.TrimSpace(os.Getenv("MILESTONE_VIEW_GROUP_ROOT_ID")),
|
|
strings.TrimSpace(config.ServerID),
|
|
"root",
|
|
"00000000-0000-0000-0000-000000000000",
|
|
}
|
|
|
|
for _, rootID := range rootCandidates {
|
|
if rootID == "" || visited[rootID] {
|
|
continue
|
|
}
|
|
|
|
children, childErr := listOperationMilestoneObjectsAtPath(
|
|
ctx,
|
|
config,
|
|
"/API/rest/v1/viewGroups/"+url.PathEscape(rootID)+"/viewGroups",
|
|
)
|
|
if childErr != nil {
|
|
continue
|
|
}
|
|
|
|
visited[rootID] = true
|
|
collected = append(collected, children...)
|
|
for _, item := range children {
|
|
collectOperationMilestoneViewGroupChildren(ctx, config, item, visited, &collected)
|
|
}
|
|
}
|
|
|
|
if len(collected) == 0 && err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return collected, nil
|
|
}
|
|
|
|
func collectOperationMilestoneViewGroupChildren(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
item map[string]any,
|
|
visited map[string]bool,
|
|
collected *[]map[string]any,
|
|
) {
|
|
id := milestoneItemID(item)
|
|
if id == "" || visited[id] {
|
|
return
|
|
}
|
|
|
|
visited[id] = true
|
|
children, err := listOperationMilestoneObjectsAtPath(
|
|
ctx,
|
|
config,
|
|
"/API/rest/v1/viewGroups/"+url.PathEscape(id)+"/viewGroups",
|
|
)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
*collected = append(*collected, children...)
|
|
for _, child := range children {
|
|
collectOperationMilestoneViewGroupChildren(ctx, config, child, visited, collected)
|
|
}
|
|
}
|
|
|
|
func operationMilestoneViewGroupParentID(viewGroups []map[string]any) string {
|
|
if parentID := strings.TrimSpace(os.Getenv("MILESTONE_VIEW_GROUP_ROOT_ID")); parentID != "" {
|
|
return parentID
|
|
}
|
|
|
|
for _, item := range viewGroups {
|
|
name := strings.ToLower(strings.TrimSpace(operationMilestoneItemDisplayName(item)))
|
|
if name == "root" || name == "ansichten" || name == "views" {
|
|
return milestoneItemID(item)
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (s *Server) ensureOperationMilestoneRoleUser(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
operation Operation,
|
|
input operationMilestoneSetupRequest,
|
|
) (*operationMilestoneRole, error) {
|
|
laptopDevice, err := s.getOperationMilestoneSetupDevice(
|
|
ctx,
|
|
input.LaptopDeviceID,
|
|
operation.Laptop,
|
|
"Laptop",
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if laptopDevice == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
operationRoleName := operationMilestoneRoleName(operation)
|
|
if operationRoleName == "" {
|
|
return nil, errors.New("Einsatznummer fehlt, Milestone-Rolle konnte nicht angelegt werden")
|
|
}
|
|
|
|
role, err := ensureOperationMilestoneRole(ctx, config, operationRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
role, err = getOperationMilestoneRole(ctx, config, role.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
roleUsers, err := listOperationMilestoneRoleUsers(ctx, config, role.ID)
|
|
if err != nil {
|
|
roleUsers = operationMilestoneObjectListFromValue(role.Data["users"])
|
|
}
|
|
|
|
usersToAssign, targetUserName, err := s.operationMilestoneRoleUsersForLaptop(
|
|
ctx,
|
|
config,
|
|
*laptopDevice,
|
|
operation,
|
|
input,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(usersToAssign) == 0 {
|
|
if targetUserName == "" {
|
|
targetUserName = operationMilestoneEvaluationRoleName(*laptopDevice, operation)
|
|
}
|
|
return nil, fmt.Errorf("AD-Benutzer %q wurde nicht gefunden", targetUserName)
|
|
}
|
|
|
|
usersToAdd := []map[string]any{}
|
|
for _, targetUser := range usersToAssign {
|
|
if milestoneItemID(targetUser) == "" {
|
|
return nil, fmt.Errorf("AD-Benutzer %q hat keine Milestone-ID", targetUserName)
|
|
}
|
|
|
|
if operationMilestoneObjectByID(roleUsers, milestoneItemID(targetUser)) != nil {
|
|
continue
|
|
}
|
|
usersToAdd = appendOperationMilestoneObjectByID(usersToAdd, targetUser)
|
|
roleUsers = appendOperationMilestoneObjectByID(roleUsers, targetUser)
|
|
}
|
|
|
|
if len(usersToAdd) > 0 {
|
|
if err := addOperationMilestoneRoleMembers(ctx, config, role.ID, usersToAdd); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
role.Data["users"] = roleUsers
|
|
|
|
return role, nil
|
|
}
|
|
|
|
func (s *Server) operationMilestoneRoleUsersForLaptop(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
laptopDevice operationMilestoneSetupDevice,
|
|
operation Operation,
|
|
input operationMilestoneSetupRequest,
|
|
) ([]map[string]any, string, error) {
|
|
if selectedUser := operationMilestoneUserFromSetupInput(input); selectedUser != nil {
|
|
return []map[string]any{selectedUser}, strings.TrimSpace(input.MilestoneUserName), nil
|
|
}
|
|
|
|
targetUserName := operationMilestoneEvaluationUserName(laptopDevice, operation)
|
|
if targetUserName != "" {
|
|
user, err := s.findOperationMilestoneActiveDirectoryUser(ctx, targetUserName)
|
|
if err != nil {
|
|
return nil, targetUserName, err
|
|
}
|
|
if user != nil {
|
|
return []map[string]any{user}, targetUserName, nil
|
|
}
|
|
}
|
|
|
|
evaluationRoleName := operationMilestoneEvaluationRoleName(laptopDevice, operation)
|
|
candidateUsers := []map[string]any{}
|
|
if evaluationRoleName != "" {
|
|
sourceRole, err := findOperationMilestoneRoleByName(ctx, config, evaluationRoleName)
|
|
if err != nil {
|
|
return nil, targetUserName, err
|
|
}
|
|
if sourceRole != nil {
|
|
sourceUsers, err := listOperationMilestoneRoleUsers(ctx, config, sourceRole.ID)
|
|
if err != nil {
|
|
return nil, targetUserName, err
|
|
}
|
|
candidateUsers = appendOperationMilestoneObjectsByID(candidateUsers, sourceUsers...)
|
|
}
|
|
}
|
|
|
|
allRoleUsers, err := listOperationMilestoneUsersFromRoles(ctx, config)
|
|
if err != nil {
|
|
return nil, targetUserName, err
|
|
}
|
|
candidateUsers = appendOperationMilestoneObjectsByID(candidateUsers, allRoleUsers...)
|
|
|
|
usersToAssign := selectOperationMilestoneRoleUsersForAssignment(candidateUsers, targetUserName, input)
|
|
if len(usersToAssign) > 0 {
|
|
return usersToAssign, targetUserName, nil
|
|
}
|
|
|
|
return nil, targetUserName, nil
|
|
}
|
|
|
|
func selectOperationMilestoneRoleUsersForAssignment(
|
|
users []map[string]any,
|
|
preferredUserName string,
|
|
input operationMilestoneSetupRequest,
|
|
) []map[string]any {
|
|
if targetUser := findOperationMilestoneUserBySetupInput(users, input); targetUser != nil {
|
|
return []map[string]any{targetUser}
|
|
}
|
|
|
|
if targetUser := findOperationMilestoneItemByName(users, preferredUserName); targetUser != nil {
|
|
return []map[string]any{targetUser}
|
|
}
|
|
|
|
if strings.TrimSpace(preferredUserName) != "" {
|
|
return nil
|
|
}
|
|
|
|
usersWithID := make([]map[string]any, 0, len(users))
|
|
for _, user := range users {
|
|
if milestoneItemID(user) != "" {
|
|
usersWithID = append(usersWithID, user)
|
|
}
|
|
}
|
|
|
|
return usersWithID
|
|
}
|
|
|
|
func hasOperationMilestoneUserSelection(input operationMilestoneSetupRequest) bool {
|
|
return strings.TrimSpace(input.MilestoneUserID) != "" ||
|
|
strings.TrimSpace(input.MilestoneUserName) != "" ||
|
|
strings.TrimSpace(input.MilestoneUserSID) != "" ||
|
|
strings.TrimSpace(input.MilestoneUserDN) != ""
|
|
}
|
|
|
|
func findOperationMilestoneUserBySetupInput(
|
|
users []map[string]any,
|
|
input operationMilestoneSetupRequest,
|
|
) map[string]any {
|
|
candidates := operationMilestoneUserSearchCandidates(input)
|
|
if len(candidates) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, user := range users {
|
|
userValues := operationMilestoneUserComparableValues(user)
|
|
for _, candidate := range candidates {
|
|
for _, userValue := range userValues {
|
|
if normalizeOperationMilestoneName(userValue) == normalizeOperationMilestoneName(candidate) {
|
|
return user
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func operationMilestoneUserSearchCandidates(input operationMilestoneSetupRequest) []string {
|
|
values := []string{
|
|
input.MilestoneUserID,
|
|
input.MilestoneUserSID,
|
|
input.MilestoneUserDN,
|
|
input.MilestoneUserName,
|
|
}
|
|
|
|
candidates := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
if value = strings.TrimSpace(value); value != "" {
|
|
candidates = append(candidates, value)
|
|
}
|
|
}
|
|
|
|
return candidates
|
|
}
|
|
|
|
func operationMilestoneUserComparableValues(user map[string]any) []string {
|
|
values := []string{
|
|
milestoneItemID(user),
|
|
operationMilestoneItemDisplayName(user),
|
|
milestoneStringValue(user["name"]),
|
|
milestoneStringValue(user["displayName"]),
|
|
milestoneStringValue(user["userName"]),
|
|
milestoneStringValue(user["username"]),
|
|
milestoneStringValue(user["accountName"]),
|
|
milestoneStringValue(user["loginName"]),
|
|
milestoneStringValue(user["sid"]),
|
|
milestoneStringValue(user["objectSid"]),
|
|
milestoneStringValue(user["distinguishedName"]),
|
|
}
|
|
|
|
relations, _ := user["relations"].(map[string]any)
|
|
for _, relationKey := range []string{"self", "user", "account"} {
|
|
if relation, ok := relations[relationKey].(map[string]any); ok {
|
|
values = append(values, milestoneStringValue(relation["id"]))
|
|
}
|
|
}
|
|
|
|
return values
|
|
}
|
|
|
|
func operationMilestoneUserFromSetupInput(input operationMilestoneSetupRequest) map[string]any {
|
|
if !hasOperationMilestoneUserSelection(input) {
|
|
return nil
|
|
}
|
|
|
|
userID := strings.TrimSpace(input.MilestoneUserID)
|
|
userSID := strings.TrimSpace(input.MilestoneUserSID)
|
|
userDN := strings.TrimSpace(input.MilestoneUserDN)
|
|
userName := strings.TrimSpace(input.MilestoneUserName)
|
|
if userID == "" {
|
|
userID = userSID
|
|
}
|
|
if userID == "" {
|
|
userID = userDN
|
|
}
|
|
if userID == "" {
|
|
userID = userName
|
|
}
|
|
if userName == "" {
|
|
userName = userID
|
|
}
|
|
|
|
user := map[string]any{
|
|
"id": userID,
|
|
"name": userName,
|
|
"displayName": userName,
|
|
"relations": map[string]any{
|
|
"self": map[string]any{
|
|
"type": "users",
|
|
"id": userID,
|
|
},
|
|
},
|
|
}
|
|
if userSID != "" {
|
|
user["sid"] = userSID
|
|
user["objectSid"] = userSID
|
|
}
|
|
if userDN != "" {
|
|
user["distinguishedName"] = userDN
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
func listOperationMilestoneUsersFromRoles(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
) ([]map[string]any, error) {
|
|
roles, err := listOperationMilestoneRoles(ctx, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
users := []map[string]any{}
|
|
for _, role := range roles {
|
|
roleUsers, err := listOperationMilestoneRoleUsers(ctx, config, role.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
users = appendOperationMilestoneObjectsByID(users, roleUsers...)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func operationMilestoneEvaluationRoleName(
|
|
laptopDevice operationMilestoneSetupDevice,
|
|
operation Operation,
|
|
) string {
|
|
candidates := []string{
|
|
laptopDevice.ComputerName,
|
|
laptopDevice.InventoryNumber,
|
|
laptopDevice.Model,
|
|
operation.Laptop,
|
|
}
|
|
|
|
for _, candidate := range candidates {
|
|
match := operationMilestoneEvaluationRolePattern.FindStringSubmatch(candidate)
|
|
if len(match) == 2 {
|
|
return "Auswertung " + match[1]
|
|
}
|
|
}
|
|
|
|
for _, candidate := range candidates {
|
|
if value := strings.TrimSpace(candidate); value != "" {
|
|
return normalizeOperationMilestoneEquipmentDisplayValue(value)
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func operationMilestoneEvaluationUserName(
|
|
laptopDevice operationMilestoneSetupDevice,
|
|
operation Operation,
|
|
) string {
|
|
candidates := []string{
|
|
laptopDevice.ComputerName,
|
|
laptopDevice.InventoryNumber,
|
|
laptopDevice.Model,
|
|
operation.Laptop,
|
|
}
|
|
|
|
for _, candidate := range candidates {
|
|
match := operationMilestoneEvaluationRolePattern.FindStringSubmatch(candidate)
|
|
if len(match) == 2 {
|
|
return "auswertung" + match[1]
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (s *Server) findOperationMilestoneActiveDirectoryUser(
|
|
ctx context.Context,
|
|
targetUserName string,
|
|
) (map[string]any, error) {
|
|
target := normalizeOperationMilestoneAccountName(targetUserName)
|
|
if target == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
credentials, err := s.getActiveDirectoryCredentials(ctx)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(credentials.ServerURL) == "" ||
|
|
strings.TrimSpace(credentials.BaseDN) == "" ||
|
|
strings.TrimSpace(credentials.BindUsername) == "" ||
|
|
strings.TrimSpace(credentials.BindPassword) == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
users, err := listActiveDirectoryUsers(ctx, credentials)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, user := range users {
|
|
if operationMilestoneActiveDirectoryUserMatches(user, target) {
|
|
return operationMilestoneUserFromActiveDirectoryUser(user), nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func operationMilestoneActiveDirectoryUserMatches(user activeDirectoryUser, target string) bool {
|
|
for _, value := range []string{
|
|
user.ID,
|
|
user.SID,
|
|
user.Username,
|
|
user.DisplayName,
|
|
user.Email,
|
|
user.DistinguishedName,
|
|
milestoneProtectedUserQualifiedName(user.Domain, user.Username),
|
|
} {
|
|
if normalizeOperationMilestoneAccountName(value) == target {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func operationMilestoneUserFromActiveDirectoryUser(user activeDirectoryUser) map[string]any {
|
|
userID := strings.TrimSpace(user.ID)
|
|
if userID == "" {
|
|
userID = strings.TrimSpace(user.SID)
|
|
}
|
|
if userID == "" {
|
|
userID = strings.TrimSpace(user.DistinguishedName)
|
|
}
|
|
if userID == "" {
|
|
userID = strings.TrimSpace(user.Username)
|
|
}
|
|
|
|
displayName := strings.TrimSpace(user.DisplayName)
|
|
if displayName == "" {
|
|
displayName = strings.TrimSpace(user.Username)
|
|
}
|
|
if displayName == "" {
|
|
displayName = userID
|
|
}
|
|
|
|
milestoneUser := map[string]any{
|
|
"id": userID,
|
|
"name": displayName,
|
|
"displayName": displayName,
|
|
"userName": strings.TrimSpace(user.Username),
|
|
"username": strings.TrimSpace(user.Username),
|
|
"email": strings.TrimSpace(user.Email),
|
|
"relations": map[string]any{
|
|
"self": map[string]any{
|
|
"type": "users",
|
|
"id": userID,
|
|
},
|
|
},
|
|
}
|
|
if domain := strings.TrimSpace(user.Domain); domain != "" {
|
|
milestoneUser["domain"] = domain
|
|
}
|
|
if sid := strings.TrimSpace(user.SID); sid != "" {
|
|
milestoneUser["sid"] = sid
|
|
milestoneUser["objectSid"] = sid
|
|
}
|
|
if dn := strings.TrimSpace(user.DistinguishedName); dn != "" {
|
|
milestoneUser["distinguishedName"] = dn
|
|
}
|
|
|
|
return milestoneUser
|
|
}
|
|
|
|
func normalizeOperationMilestoneAccountName(value string) string {
|
|
value = strings.TrimSpace(strings.ToLower(value))
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
if _, account, ok := strings.Cut(value, "\\"); ok {
|
|
value = account
|
|
}
|
|
if account, _, ok := strings.Cut(value, "@"); ok {
|
|
value = account
|
|
}
|
|
|
|
replacer := strings.NewReplacer(" ", "", "\t", "", "\n", "", "\r", "", "-", "", "_", "")
|
|
return replacer.Replace(value)
|
|
}
|
|
|
|
func (s *Server) assignOperationMilestoneCameraToRole(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
role operationMilestoneRole,
|
|
cameraDevice operationMilestoneSetupDevice,
|
|
) (string, error) {
|
|
cameraID, err := s.getOperationMilestoneCameraDeviceID(ctx, cameraDevice.MilestoneHardwareID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if cameraID == "" {
|
|
return "", fmt.Errorf("Keine Milestone-Kamera fuer Hardware %q gefunden", cameraDevice.MilestoneHardwareID)
|
|
}
|
|
|
|
return operationMilestonePermissionWriteUnsupportedMessage(role, "Kamera"), nil
|
|
}
|
|
|
|
func (s *Server) assignOperationMilestoneViewGroupToRole(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
role operationMilestoneRole,
|
|
viewGroupID string,
|
|
) (string, error) {
|
|
if strings.TrimSpace(viewGroupID) == "" {
|
|
return "", errors.New("Ansichtsgruppen-ID fehlt")
|
|
}
|
|
|
|
return operationMilestonePermissionWriteUnsupportedMessage(role, "Ansichtsgruppe"), nil
|
|
}
|
|
|
|
func operationMilestonePermissionWriteUnsupportedMessage(
|
|
role operationMilestoneRole,
|
|
objectLabel string,
|
|
) string {
|
|
return fmt.Sprintf(
|
|
"%s ist vorhanden. Berechtigungen fuer Rolle %q werden in Milestone ueber Security-Namespaces/Gruppenrechte verwaltet und nicht ueber das Rollenfeld gesetzt.",
|
|
objectLabel,
|
|
operationMilestoneRoleDisplayName(role),
|
|
)
|
|
}
|
|
|
|
func (s *Server) getOperationMilestoneCameraDeviceID(
|
|
ctx context.Context,
|
|
milestoneHardwareID string,
|
|
) (string, error) {
|
|
var cameraID string
|
|
|
|
err := s.db.QueryRow(
|
|
ctx,
|
|
`
|
|
SELECT COALESCE(milestone_device_id, '')
|
|
FROM milestone_hardware_child_devices
|
|
WHERE milestone_hardware_id = $1
|
|
AND lower(device_type) = 'camera'
|
|
ORDER BY name ASC, display_name ASC
|
|
LIMIT 1
|
|
`,
|
|
milestoneHardwareID,
|
|
).Scan(&cameraID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", nil
|
|
}
|
|
|
|
return cameraID, err
|
|
}
|
|
|
|
func (s *Server) countOperationMilestoneEvaluationRoles(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
) (int, error) {
|
|
roles, err := listOperationMilestoneRoles(ctx, config)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
count := 0
|
|
for _, role := range roles {
|
|
if operationMilestoneEvaluationRolePattern.MatchString(operationMilestoneRoleDisplayName(role)) {
|
|
count++
|
|
}
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func listOperationMilestoneRoles(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
) ([]operationMilestoneRole, error) {
|
|
items, err := listOperationMilestoneObjectsAtPath(ctx, config, "/API/rest/v1/roles")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
roles := make([]operationMilestoneRole, 0, len(items))
|
|
for _, item := range items {
|
|
role := operationMilestoneRoleFromItem(item)
|
|
if role.ID == "" {
|
|
continue
|
|
}
|
|
|
|
roles = append(roles, role)
|
|
}
|
|
|
|
return roles, nil
|
|
}
|
|
|
|
func getOperationMilestoneRole(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
roleID string,
|
|
) (*operationMilestoneRole, error) {
|
|
body, _, err := operationMilestoneRequestJSON(
|
|
ctx,
|
|
config,
|
|
http.MethodGet,
|
|
"/API/rest/v1/roles/"+url.PathEscape(roleID),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
role := operationMilestoneRoleFromItem(decodeOperationMilestoneObject(body))
|
|
if role.ID == "" {
|
|
return nil, fmt.Errorf("Milestone-Rolle %q konnte nicht geladen werden", roleID)
|
|
}
|
|
|
|
return &role, nil
|
|
}
|
|
|
|
func findOperationMilestoneRoleByName(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
roleName string,
|
|
) (*operationMilestoneRole, error) {
|
|
roles, err := listOperationMilestoneRoles(ctx, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
normalizedRoleName := normalizeOperationMilestoneName(roleName)
|
|
for _, role := range roles {
|
|
if normalizeOperationMilestoneName(role.Name) == normalizedRoleName ||
|
|
normalizeOperationMilestoneName(role.DisplayName) == normalizedRoleName {
|
|
return &role, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func ensureOperationMilestoneRole(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
roleName string,
|
|
) (*operationMilestoneRole, error) {
|
|
roleName = strings.TrimSpace(roleName)
|
|
if roleName == "" {
|
|
return nil, errors.New("Rollenname fehlt")
|
|
}
|
|
|
|
if role, err := findOperationMilestoneRoleByName(ctx, config, roleName); err != nil {
|
|
return nil, err
|
|
} else if role != nil {
|
|
return role, nil
|
|
}
|
|
|
|
payloads := []map[string]any{
|
|
{
|
|
"name": roleName,
|
|
"displayName": roleName,
|
|
},
|
|
{
|
|
"name": roleName,
|
|
},
|
|
}
|
|
|
|
var lastErr error
|
|
for _, payload := range payloads {
|
|
body, _, err := operationMilestoneRequestJSON(
|
|
ctx,
|
|
config,
|
|
http.MethodPost,
|
|
"/API/rest/v1/roles",
|
|
payload,
|
|
)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
role := operationMilestoneRoleFromItem(decodeOperationMilestoneObject(body))
|
|
if role.ID != "" {
|
|
role.Created = true
|
|
return &role, nil
|
|
}
|
|
|
|
roleFromList, err := findOperationMilestoneRoleByName(ctx, config, roleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if roleFromList != nil {
|
|
roleFromList.Created = true
|
|
return roleFromList, nil
|
|
}
|
|
}
|
|
|
|
if lastErr != nil {
|
|
return nil, lastErr
|
|
}
|
|
|
|
return nil, fmt.Errorf("Milestone hat keine ID fuer Rolle %q zurueckgegeben", roleName)
|
|
}
|
|
|
|
func operationMilestoneRoleFromItem(item map[string]any) operationMilestoneRole {
|
|
return operationMilestoneRole{
|
|
ID: milestoneItemID(item),
|
|
Name: strings.TrimSpace(milestoneStringValue(item["name"])),
|
|
DisplayName: strings.TrimSpace(milestoneStringValue(item["displayName"])),
|
|
Data: item,
|
|
}
|
|
}
|
|
|
|
func operationMilestoneRoleDisplayName(role operationMilestoneRole) string {
|
|
if role.DisplayName != "" {
|
|
return role.DisplayName
|
|
}
|
|
if role.Name != "" {
|
|
return role.Name
|
|
}
|
|
|
|
return role.ID
|
|
}
|
|
|
|
func listOperationMilestoneRoleUsers(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
roleID string,
|
|
) ([]map[string]any, error) {
|
|
return listOperationMilestoneObjectsAtPath(
|
|
ctx,
|
|
config,
|
|
"/API/rest/v1/roles/"+url.PathEscape(roleID)+"/users",
|
|
)
|
|
}
|
|
|
|
func putOperationMilestoneRole(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
role operationMilestoneRole,
|
|
users []map[string]any,
|
|
) error {
|
|
if role.Data == nil {
|
|
role.Data = map[string]any{}
|
|
}
|
|
|
|
roleName := strings.TrimSpace(role.Name)
|
|
if roleName == "" {
|
|
roleName = strings.TrimSpace(role.DisplayName)
|
|
}
|
|
if roleName == "" {
|
|
roleName = role.ID
|
|
}
|
|
roleDisplayName := strings.TrimSpace(role.DisplayName)
|
|
if roleDisplayName == "" {
|
|
roleDisplayName = strings.TrimSpace(milestoneStringValue(role.Data["displayName"]))
|
|
}
|
|
if roleDisplayName == "" {
|
|
roleDisplayName = roleName
|
|
}
|
|
|
|
relations, ok := role.Data["relations"].(map[string]any)
|
|
if !ok || relations == nil {
|
|
relations = map[string]any{
|
|
"self": map[string]any{
|
|
"type": "roles",
|
|
"id": role.ID,
|
|
},
|
|
}
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"allowMobileClientLogOn": milestoneBoolValue(role.Data["allowMobileClientLogOn"]),
|
|
"allowSmartClientLogOn": milestoneBoolValue(role.Data["allowSmartClientLogOn"]),
|
|
"allowWebClientLogOn": milestoneBoolValue(role.Data["allowWebClientLogOn"]),
|
|
"description": milestoneStringValue(role.Data["description"]),
|
|
"displayName": roleDisplayName,
|
|
"dualAuthorizationRequired": milestoneBoolValue(role.Data["dualAuthorizationRequired"]),
|
|
"makeUsersAnonymousDuringPTZSession": milestoneBoolValue(role.Data["makeUsersAnonymousDuringPTZSession"]),
|
|
"name": roleName,
|
|
"relations": relations,
|
|
"users": users,
|
|
}
|
|
|
|
_, _, err := operationMilestoneRequestJSON(
|
|
ctx,
|
|
config,
|
|
http.MethodPut,
|
|
"/API/rest/v1/roles/"+url.PathEscape(role.ID),
|
|
payload,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
func patchOperationMilestoneRole(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
roleID string,
|
|
payload map[string]any,
|
|
) error {
|
|
_, _, err := operationMilestoneRequestJSON(
|
|
ctx,
|
|
config,
|
|
http.MethodPatch,
|
|
"/API/rest/v1/roles/"+url.PathEscape(roleID),
|
|
payload,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
func listOperationMilestoneObjectsAtPath(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
path string,
|
|
) ([]map[string]any, error) {
|
|
body, _, err := operationMilestoneRequestJSON(ctx, config, http.MethodGet, path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return decodeMilestoneObjectItems(body)
|
|
}
|
|
|
|
func operationMilestoneRequestJSON(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
method string,
|
|
path string,
|
|
payload any,
|
|
) ([]byte, int, error) {
|
|
var bodyReader io.Reader
|
|
if payload != nil {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
bodyReader = bytes.NewReader(body)
|
|
}
|
|
|
|
requestURL := strings.TrimRight(config.Host, "/") + path
|
|
request, err := http.NewRequestWithContext(ctx, method, requestURL, bodyReader)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
setMilestoneRequestHeaders(request, config, payload != nil)
|
|
|
|
response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
responseBody, readErr := io.ReadAll(response.Body)
|
|
if readErr != nil {
|
|
return nil, response.StatusCode, readErr
|
|
}
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return responseBody, response.StatusCode, fmt.Errorf(
|
|
"milestone %s %s failed: status=%d body=%s",
|
|
method,
|
|
path,
|
|
response.StatusCode,
|
|
truncateMilestoneLogBody(responseBody),
|
|
)
|
|
}
|
|
|
|
return responseBody, response.StatusCode, nil
|
|
}
|
|
|
|
func decodeOperationMilestoneObject(body []byte) map[string]any {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(body, &decoded); err != nil {
|
|
return map[string]any{}
|
|
}
|
|
|
|
if data, ok := decoded["data"].(map[string]any); ok {
|
|
return data
|
|
}
|
|
|
|
return decoded
|
|
}
|
|
|
|
func findOperationMilestoneItemIDByName(items []map[string]any, name string) string {
|
|
item := findOperationMilestoneItemByName(items, name)
|
|
if item == nil {
|
|
return ""
|
|
}
|
|
|
|
return milestoneItemID(item)
|
|
}
|
|
|
|
func findOperationMilestoneItemByName(items []map[string]any, name string) map[string]any {
|
|
normalizedName := normalizeOperationMilestoneName(name)
|
|
if normalizedName == "" {
|
|
return nil
|
|
}
|
|
|
|
for _, item := range items {
|
|
names := []string{
|
|
operationMilestoneItemDisplayName(item),
|
|
milestoneStringValue(item["name"]),
|
|
milestoneStringValue(item["userName"]),
|
|
milestoneStringValue(item["username"]),
|
|
milestoneStringValue(item["accountName"]),
|
|
milestoneStringValue(item["loginName"]),
|
|
}
|
|
|
|
for _, candidate := range names {
|
|
if normalizeOperationMilestoneName(candidate) == normalizedName {
|
|
return item
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func operationMilestoneItemDisplayName(item map[string]any) string {
|
|
if value := strings.TrimSpace(milestoneStringValue(item["displayName"])); value != "" {
|
|
return value
|
|
}
|
|
if value := strings.TrimSpace(milestoneStringValue(item["name"])); value != "" {
|
|
return value
|
|
}
|
|
|
|
return milestoneItemID(item)
|
|
}
|
|
|
|
func milestoneItemID(item map[string]any) string {
|
|
if item == nil {
|
|
return ""
|
|
}
|
|
|
|
return milestoneDeviceID(item)
|
|
}
|
|
|
|
func normalizeOperationMilestoneName(value string) string {
|
|
return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(value)), " "))
|
|
}
|
|
|
|
func operationMilestoneObjectListFromValue(value any) []map[string]any {
|
|
rawItems, ok := value.([]any)
|
|
if !ok {
|
|
return []map[string]any{}
|
|
}
|
|
|
|
items := make([]map[string]any, 0, len(rawItems))
|
|
for _, rawItem := range rawItems {
|
|
if item, ok := rawItem.(map[string]any); ok {
|
|
items = append(items, item)
|
|
}
|
|
}
|
|
|
|
return items
|
|
}
|
|
|
|
func appendOperationMilestoneObjectByID(
|
|
items []map[string]any,
|
|
nextItem map[string]any,
|
|
) []map[string]any {
|
|
nextID := milestoneItemID(nextItem)
|
|
if nextID == "" {
|
|
return items
|
|
}
|
|
|
|
for _, item := range items {
|
|
if strings.EqualFold(milestoneItemID(item), nextID) {
|
|
return items
|
|
}
|
|
}
|
|
|
|
return append(items, nextItem)
|
|
}
|
|
|
|
func appendOperationMilestoneObjectsByID(
|
|
items []map[string]any,
|
|
nextItems ...map[string]any,
|
|
) []map[string]any {
|
|
for _, nextItem := range nextItems {
|
|
items = appendOperationMilestoneObjectByID(items, nextItem)
|
|
}
|
|
|
|
return items
|
|
}
|
|
|
|
func milestoneReferenceListFromItems(items []map[string]any) []map[string]any {
|
|
refs := make([]map[string]any, 0, len(items))
|
|
for _, item := range items {
|
|
if id := milestoneItemID(item); id != "" {
|
|
refs = appendMilestoneReference(refs, id)
|
|
}
|
|
}
|
|
|
|
return refs
|
|
}
|
|
|
|
func milestoneReferenceListFromValue(value any) []map[string]any {
|
|
rawItems, ok := value.([]any)
|
|
if !ok {
|
|
return []map[string]any{}
|
|
}
|
|
|
|
refs := make([]map[string]any, 0, len(rawItems))
|
|
for _, rawItem := range rawItems {
|
|
if item, ok := rawItem.(map[string]any); ok {
|
|
if id := milestoneItemID(item); id != "" {
|
|
refs = appendMilestoneReference(refs, id)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if id := strings.TrimSpace(milestoneStringValue(rawItem)); id != "" {
|
|
refs = appendMilestoneReference(refs, id)
|
|
}
|
|
}
|
|
|
|
return refs
|
|
}
|
|
|
|
func appendMilestoneReference(refs []map[string]any, id string) []map[string]any {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return refs
|
|
}
|
|
|
|
for _, ref := range refs {
|
|
if strings.EqualFold(strings.TrimSpace(milestoneStringValue(ref["id"])), id) {
|
|
return refs
|
|
}
|
|
}
|
|
|
|
return append(refs, map[string]any{
|
|
"id": id,
|
|
})
|
|
}
|