package main import ( "context" "errors" "fmt" "log" "net/http" "net/url" "strings" "github.com/jackc/pgx/v5" ) type milestoneRoleResponse struct { ID string `json:"id"` Name string `json:"name"` DisplayName string `json:"displayName"` Description string `json:"description"` Users []milestoneRoleUserItem `json:"users,omitempty"` UserCount int `json:"userCount"` CanEdit bool `json:"canEdit"` CanDelete bool `json:"canDelete"` } type milestoneRoleUserItem struct { ID string `json:"id"` DisplayName string `json:"displayName"` Name string `json:"name"` UserName string `json:"userName"` Username string `json:"username"` Email string `json:"email"` Domain string `json:"domain"` SID string `json:"sid"` DistinguishedName string `json:"distinguishedName"` Source string `json:"source"` } type saveMilestoneRoleRequest struct { Name string `json:"name"` DisplayName string `json:"displayName"` Description string `json:"description"` } type addMilestoneRoleUsersRequest struct { Users []milestoneRoleUserInput `json:"users"` } type milestoneRoleUserInput struct { ID string `json:"id"` DisplayName string `json:"displayName"` Username string `json:"username"` UserName string `json:"userName"` Name string `json:"name"` Email string `json:"email"` Domain string `json:"domain"` DistinguishedName string `json:"distinguishedName"` SID string `json:"sid"` Source string `json:"source"` } func writeMilestoneRoleActionError(w http.ResponseWriter, status int, message string, err error) { if err != nil { log.Printf("%s: %v", message, err) } payload := map[string]string{ "error": message, } if err != nil { payload["details"] = err.Error() } writeJSON(w, status, payload) } func (s *Server) handleAdminListMilestoneRoles(w http.ResponseWriter, r *http.Request) { currentUser, _ := userFromContext(r.Context()) isAdministrator := userHasRight(currentUser, "admin") config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } roles, err := listOperationMilestoneRoles(r.Context(), config) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rollen konnten nicht geladen werden") return } responseRoles := make([]milestoneRoleResponse, 0, len(roles)) for _, role := range roles { if milestoneRoleIsAdministrators(role) && !isAdministrator { continue } responseRole := milestoneRoleResponseFromRole(role, nil) responseRoles = append(responseRoles, responseRole) } writeJSON(w, http.StatusOK, map[string]any{ "roles": responseRoles, }) } func (s *Server) handleAdminGetMilestoneRole(w http.ResponseWriter, r *http.Request) { roleID := strings.TrimSpace(r.PathValue("id")) if roleID == "" { writeError(w, http.StatusBadRequest, "Rollen-ID fehlt") return } currentUser, _ := userFromContext(r.Context()) isAdministrator := userHasRight(currentUser, "admin") config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } role, err := getOperationMilestoneRole(r.Context(), config, roleID) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle konnte nicht geladen werden") return } if milestoneRoleIsAdministrators(*role) && !isAdministrator { writeError(w, http.StatusNotFound, "Milestone-Rolle wurde nicht gefunden") return } users, err := listOperationMilestoneRoleUsers(r.Context(), config, roleID) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rollenbenutzer konnten nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "role": milestoneRoleResponseFromRole(*role, users), }) } func (s *Server) handleAdminCreateMilestoneRole(w http.ResponseWriter, r *http.Request) { var input saveMilestoneRoleRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } roleName := strings.TrimSpace(input.Name) if roleName == "" { roleName = strings.TrimSpace(input.DisplayName) } if roleName == "" { writeError(w, http.StatusBadRequest, "Rollenname ist erforderlich") return } config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } existingRole, err := findOperationMilestoneRoleByName(r.Context(), config, roleName) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rollen konnten nicht geprüft werden") return } if existingRole != nil { writeError(w, http.StatusConflict, "Eine Rolle mit diesem Namen existiert bereits") return } payload := map[string]any{ "name": roleName, "displayName": strings.TrimSpace(input.DisplayName), "description": strings.TrimSpace(input.Description), } if strings.TrimSpace(input.DisplayName) == "" { payload["displayName"] = roleName } body, _, err := operationMilestoneRequestJSON( r.Context(), config, http.MethodPost, "/API/rest/v1/roles", payload, ) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle konnte nicht angelegt werden") return } role := operationMilestoneRoleFromItem(decodeOperationMilestoneObject(body)) if role.ID == "" { roleFromList, err := findOperationMilestoneRoleByName(r.Context(), config, roleName) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle wurde angelegt, konnte aber nicht geladen werden") return } if roleFromList == nil { writeError(w, http.StatusBadGateway, "Milestone hat keine Rollen-ID zurückgegeben") return } role = *roleFromList } roleDetail, err := getOperationMilestoneRole(r.Context(), config, role.ID) if err == nil && roleDetail != nil { role = *roleDetail } writeJSON(w, http.StatusCreated, map[string]any{ "role": milestoneRoleResponseFromRole(role, nil), }) } func (s *Server) handleAdminUpdateMilestoneRole(w http.ResponseWriter, r *http.Request) { roleID := strings.TrimSpace(r.PathValue("id")) if roleID == "" { writeError(w, http.StatusBadRequest, "Rollen-ID fehlt") return } currentUser, _ := userFromContext(r.Context()) isAdministrator := userHasRight(currentUser, "admin") var input saveMilestoneRoleRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } roleName := strings.TrimSpace(input.Name) if roleName == "" { roleName = strings.TrimSpace(input.DisplayName) } if roleName == "" { writeError(w, http.StatusBadRequest, "Rollenname ist erforderlich") return } config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } role, err := getOperationMilestoneRole(r.Context(), config, roleID) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle konnte nicht geladen werden") return } if milestoneRoleIsAdministrators(*role) && !isAdministrator { writeError(w, http.StatusNotFound, "Milestone-Rolle wurde nicht gefunden") return } role.Name = roleName role.DisplayName = strings.TrimSpace(input.DisplayName) if role.DisplayName == "" { role.DisplayName = roleName } if err := patchOperationMilestoneRole(r.Context(), config, role.ID, map[string]any{ "name": roleName, "displayName": role.DisplayName, "description": strings.TrimSpace(input.Description), }); err != nil { writeMilestoneRoleActionError(w, http.StatusBadGateway, "Milestone-Rolle konnte nicht gespeichert werden", err) return } role, err = getOperationMilestoneRole(r.Context(), config, roleID) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle wurde gespeichert, konnte aber nicht geladen werden") return } users, _ := listOperationMilestoneRoleUsers(r.Context(), config, roleID) writeJSON(w, http.StatusOK, map[string]any{ "role": milestoneRoleResponseFromRole(*role, users), }) } func (s *Server) handleAdminDeleteMilestoneRole(w http.ResponseWriter, r *http.Request) { roleID := strings.TrimSpace(r.PathValue("id")) if roleID == "" { writeError(w, http.StatusBadRequest, "Rollen-ID fehlt") return } config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } role, err := getOperationMilestoneRole(r.Context(), config, roleID) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle konnte nicht geladen werden") return } if milestoneRoleIsAdministrators(*role) { writeError(w, http.StatusForbidden, "Die Rolle Administrators darf nicht gelöscht werden") return } _, _, err = operationMilestoneRequestJSON( r.Context(), config, http.MethodDelete, "/API/rest/v1/roles/"+url.PathEscape(roleID), nil, ) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle konnte nicht gelöscht werden") return } writeJSON(w, http.StatusOK, map[string]any{"ok": true}) } func (s *Server) handleAdminAddMilestoneRoleUsers(w http.ResponseWriter, r *http.Request) { roleID := strings.TrimSpace(r.PathValue("id")) if roleID == "" { writeError(w, http.StatusBadRequest, "Rollen-ID fehlt") return } currentUser, _ := userFromContext(r.Context()) isAdministrator := userHasRight(currentUser, "admin") var input addMilestoneRoleUsersRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } if len(input.Users) == 0 { writeError(w, http.StatusBadRequest, "Keine Benutzer ausgewählt") return } config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } role, users, err := getMilestoneAdminRoleWithUsers(r.Context(), config, roleID) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle konnte nicht geladen werden") return } if milestoneRoleIsAdministrators(*role) && !isAdministrator { writeError(w, http.StatusNotFound, "Milestone-Rolle wurde nicht gefunden") return } nextUsers := users usersToAdd := []map[string]any{} for _, userInput := range input.Users { user := milestoneRoleUserReferenceFromInput(userInput) if milestoneItemID(user) == "" || milestoneRoleUserItemIsHidden(milestoneRoleUserItemFromObject(user)) { continue } if operationMilestoneObjectByID(nextUsers, milestoneItemID(user)) != nil { continue } nextUsers = appendOperationMilestoneObjectByID(nextUsers, user) usersToAdd = appendOperationMilestoneObjectByID(usersToAdd, user) } if len(usersToAdd) == 0 { writeError(w, http.StatusBadRequest, "Kein gültiger Benutzer ausgewählt") return } if err := addOperationMilestoneRoleMembers(r.Context(), config, role.ID, usersToAdd); err != nil { writeMilestoneRoleActionError(w, http.StatusBadGateway, "Benutzer konnten der Milestone-Rolle nicht hinzugefügt werden", err) return } role, users, err = getMilestoneAdminRoleWithUsers(r.Context(), config, roleID) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle wurde gespeichert, konnte aber nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "role": milestoneRoleResponseFromRole(*role, users), }) } func (s *Server) handleAdminRemoveMilestoneRoleUser(w http.ResponseWriter, r *http.Request) { roleID := strings.TrimSpace(r.PathValue("id")) userID := strings.TrimSpace(r.PathValue("userId")) if roleID == "" || userID == "" { writeError(w, http.StatusBadRequest, "Rollen-ID oder Benutzer-ID fehlt") return } currentUser, _ := userFromContext(r.Context()) isAdministrator := userHasRight(currentUser, "admin") config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } role, users, err := getMilestoneAdminRoleWithUsers(r.Context(), config, roleID) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle konnte nicht geladen werden") return } if milestoneRoleIsAdministrators(*role) && !isAdministrator { writeError(w, http.StatusNotFound, "Milestone-Rolle wurde nicht gefunden") return } nextUsers := make([]map[string]any, 0, len(users)) usersToRemove := []map[string]any{} for _, user := range users { if strings.EqualFold(milestoneItemID(user), userID) { if milestoneRoleUserItemIsHidden(milestoneRoleUserItemFromObject(user)) { writeError(w, http.StatusForbidden, "Dieser Benutzer darf nicht entfernt werden") return } usersToRemove = append(usersToRemove, user) continue } nextUsers = append(nextUsers, user) } if len(usersToRemove) == 0 { writeError(w, http.StatusNotFound, "Benutzer wurde in der Rolle nicht gefunden") return } if err := removeOperationMilestoneRoleMembers(r.Context(), config, role.ID, usersToRemove); err != nil { writeMilestoneRoleActionError(w, http.StatusBadGateway, "Benutzer konnte nicht aus der Milestone-Rolle entfernt werden", err) return } role, users, err = getMilestoneAdminRoleWithUsers(r.Context(), config, roleID) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Rolle wurde gespeichert, konnte aber nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "role": milestoneRoleResponseFromRole(*role, users), }) } func (s *Server) handleAdminListActiveDirectoryUsers(w http.ResponseWriter, r *http.Request) { credentials, err := s.getActiveDirectoryCredentials(r.Context()) if errors.Is(err, pgx.ErrNoRows) { writeJSON(w, http.StatusOK, map[string]any{ "configured": false, "users": []activeDirectoryUser{}, }) return } if err != nil { writeError(w, http.StatusInternalServerError, "AD-Einstellungen konnten nicht geladen werden") return } if !credentials.Configured() { writeJSON(w, http.StatusOK, map[string]any{ "configured": false, "users": []activeDirectoryUser{}, }) return } users, err := listActiveDirectoryUsers(r.Context(), credentials) if err != nil { logError("AD-Benutzer konnten nicht geladen werden", err, LogFields{ "baseDn": credentials.BaseDN, }) writeError(w, http.StatusBadGateway, "AD-Benutzer konnten nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "configured": true, "baseDn": credentials.BaseDN, "users": users, }) } func (s *Server) handleAdminListMilestoneBasicUsers(w http.ResponseWriter, r *http.Request) { config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } items, err := listOperationMilestoneObjectsAtPath(r.Context(), config, "/API/rest/v1/basicUsers") if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Basisbenutzer konnten nicht geladen werden") return } users := make([]milestoneRoleUserItem, 0, len(items)) for _, item := range items { user := milestoneRoleUserItemFromObject(item) if user.ID == "" || milestoneRoleUserItemIsHidden(user) { continue } user.Source = "basic" user.Domain = "" users = append(users, user) } writeJSON(w, http.StatusOK, map[string]any{ "users": users, }) } func getMilestoneAdminRoleWithUsers( ctx context.Context, config milestoneRuntimeConfig, roleID string, ) (*operationMilestoneRole, []map[string]any, error) { role, err := getOperationMilestoneRole(ctx, config, roleID) if err != nil { return nil, nil, err } users, err := listOperationMilestoneRoleUsers(ctx, config, roleID) if err != nil { return nil, nil, err } return role, users, nil } func milestoneRoleResponseFromRole( role operationMilestoneRole, users []map[string]any, ) milestoneRoleResponse { roleData := role.Data if roleData == nil { roleData = map[string]any{} } if users == nil { users = operationMilestoneObjectListFromValue(roleData["users"]) } responseUsers := make([]milestoneRoleUserItem, 0, len(users)) for _, user := range users { if item := milestoneRoleUserItemFromObject(user); item.ID != "" { if milestoneRoleUserItemIsHidden(item) { continue } responseUsers = append(responseUsers, item) } } canDelete := !milestoneRoleIsAdministrators(role) return milestoneRoleResponse{ ID: role.ID, Name: role.Name, DisplayName: operationMilestoneRoleDisplayName(role), Description: strings.TrimSpace(milestoneStringValue(roleData["description"])), Users: responseUsers, UserCount: len(responseUsers), CanEdit: true, CanDelete: canDelete, } } func milestoneRoleUserItemFromObject(user map[string]any) milestoneRoleUserItem { id := milestoneItemID(user) displayName := strings.TrimSpace(operationMilestoneItemDisplayName(user)) name := strings.TrimSpace(milestoneStringValue(user["name"])) userName := strings.TrimSpace(milestoneStringValue(user["userName"])) if userName == "" { userName = strings.TrimSpace(milestoneStringValue(user["username"])) } if userName == "" { userName = strings.TrimSpace(milestoneStringValue(user["accountName"])) } if userName == "" { userName = strings.TrimSpace(milestoneStringValue(user["loginName"])) } email := strings.TrimSpace(milestoneStringValue(user["email"])) if email == "" { email = strings.TrimSpace(milestoneStringValue(user["mail"])) } sid := strings.TrimSpace(milestoneStringValue(user["sid"])) if sid == "" { sid = strings.TrimSpace(milestoneStringValue(user["objectSid"])) } distinguishedName := strings.TrimSpace(milestoneStringValue(user["distinguishedName"])) source := milestoneRoleUserSourceFromObject(user) domain := milestoneRoleUserDomainFromObject(user) if splitDomain, splitUserName := splitMilestoneDomainUser(userName); splitDomain != "" { if domain == "" { domain = splitDomain } userName = splitUserName } if splitDomain, splitName := splitMilestoneDomainUser(name); splitDomain != "" { if domain == "" { domain = splitDomain } name = splitName } if splitDomain, splitDisplayName := splitMilestoneDomainUser(displayName); splitDomain != "" { if domain == "" { domain = splitDomain } if userName == "" { userName = splitDisplayName } displayName = splitDisplayName } if domain == "" { domain = activeDirectoryDomainFromPrincipal(milestoneStringValue(user["userPrincipalName"])) } if domain == "" && email != "" { domain = activeDirectoryDomainFromPrincipal(email) } if domain == "" && distinguishedName != "" { domain = activeDirectoryDomainFromDN(distinguishedName) } if milestoneRoleUserDomainIsBasic(domain) { source = "basic" domain = "" } if source == "basic" { domain = "" } if displayName == "" { displayName = userName } if displayName == "" { displayName = name } if displayName == "" { displayName = id } return milestoneRoleUserItem{ ID: id, DisplayName: displayName, Name: name, UserName: userName, Username: userName, Email: email, Domain: domain, SID: sid, DistinguishedName: distinguishedName, Source: source, } } func milestoneRoleUserReferenceFromInput(input milestoneRoleUserInput) map[string]any { userID := strings.TrimSpace(input.ID) userSID := strings.TrimSpace(input.SID) userDN := strings.TrimSpace(input.DistinguishedName) displayName := strings.TrimSpace(input.DisplayName) username := strings.TrimSpace(input.Username) if username == "" { username = strings.TrimSpace(input.UserName) } name := strings.TrimSpace(input.Name) email := strings.TrimSpace(input.Email) domain := strings.TrimSpace(input.Domain) source := milestoneRoleUserSource(input.Source) if source == "" { source = "ad" } if milestoneRoleUserDomainIsBasic(domain) { source = "basic" domain = "" } if splitDomain, splitUsername := splitMilestoneDomainUser(username); splitDomain != "" { if domain == "" && source != "basic" { domain = splitDomain } username = splitUsername } if splitDomain, splitName := splitMilestoneDomainUser(name); splitDomain != "" { if domain == "" && source != "basic" { domain = splitDomain } name = splitName } if splitDomain, splitDisplayName := splitMilestoneDomainUser(displayName); splitDomain != "" { if domain == "" && source != "basic" { domain = splitDomain } displayName = splitDisplayName } if source == "basic" { domain = "" } if userID == "" { userID = userSID } if userID == "" { userID = userDN } if userID == "" { userID = username } if displayName == "" { displayName = name } if displayName == "" { displayName = username } if displayName == "" { displayName = email } if displayName == "" { displayName = userID } user := map[string]any{ "id": userID, "name": displayName, "displayName": displayName, "userName": username, "username": username, "email": email, "relations": map[string]any{ "self": map[string]any{ "type": milestoneRoleUserRelationType(source), "id": userID, }, }, } if domain != "" { user["domain"] = domain } if userSID != "" { user["sid"] = userSID user["objectSid"] = userSID } if userDN != "" { user["distinguishedName"] = userDN } return user } func addOperationMilestoneRoleMembers( ctx context.Context, config milestoneRuntimeConfig, roleID string, users []map[string]any, ) error { return postOperationMilestoneRoleMemberTasks(ctx, config, roleID, "AddRoleMember", users) } func removeOperationMilestoneRoleMembers( ctx context.Context, config milestoneRuntimeConfig, roleID string, users []map[string]any, ) error { return postOperationMilestoneRoleMemberTasks(ctx, config, roleID, "RemoveRoleMember", users) } func postOperationMilestoneRoleMemberTasks( ctx context.Context, config milestoneRuntimeConfig, roleID string, taskName string, users []map[string]any, ) error { roleID = strings.TrimSpace(roleID) taskName = strings.TrimSpace(taskName) if roleID == "" { return errors.New("role id is missing") } if taskName == "" { return errors.New("role member task name is missing") } for _, user := range users { payload, err := milestoneRoleMemberTaskPayload(user) if err != nil { return err } if len(payload) == 0 { continue } body, _, err := operationMilestoneRequestJSON( ctx, config, http.MethodPost, "/API/rest/v1/roles/"+url.PathEscape(roleID)+"/users?task="+url.QueryEscape(taskName), payload, ) if err != nil { return err } if result, ok := milestoneTaskResultFromBody(body); ok && milestoneTaskResultIsError(result) { return fmt.Errorf( "milestone %s returned error state=%s errorCode=%s errorText=%s", taskName, result.State, result.ErrorCode, result.ErrorText, ) } } return nil } func milestoneRoleMemberTaskItemSelection(user map[string]any) map[string]any { userID := strings.TrimSpace(milestoneItemID(user)) if userID == "" { return nil } source := milestoneRoleUserSourceFromObject(user) itemSelection := map[string]any{ "type": milestoneRoleUserRelationType(source), "id": userID, } return itemSelection } func milestoneRoleMemberTaskPayload(user map[string]any) (map[string]any, error) { itemSelection := milestoneRoleMemberTaskItemSelection(user) userID := strings.TrimSpace(milestoneStringValue(itemSelection["id"])) if userID == "" { return nil, nil } payload := map[string]any{ "itemSelection": itemSelection, } if milestoneRoleUserSourceFromObject(user) != "basic" { sid := strings.TrimSpace(milestoneStringValue(user["sid"])) if sid == "" { sid = strings.TrimSpace(milestoneStringValue(user["objectSid"])) } if sid == "" && strings.HasPrefix(strings.ToUpper(userID), "S-") { sid = userID } if sid == "" { return nil, fmt.Errorf( "Windows-Benutzer %q hat keine SID. AddRoleMember erwartet den Parameter sid.", operationMilestoneItemDisplayName(user), ) } payload["sid"] = sid } return payload, nil } func operationMilestoneObjectByID(items []map[string]any, id string) map[string]any { id = strings.TrimSpace(id) if id == "" { return nil } for _, item := range items { if strings.EqualFold(milestoneItemID(item), id) { return item } } return nil } func milestoneRoleUserSourceFromObject(user map[string]any) string { if milestoneRoleUserDomainIsBasic(milestoneRoleUserDomainFromObject(user)) { return "basic" } source := milestoneRoleUserSource(milestoneStringValue(user["source"])) if source != "" { return source } if relations, ok := user["relations"].(map[string]any); ok { if self, ok := relations["self"].(map[string]any); ok { relationType := strings.TrimSpace(milestoneStringValue(self["type"])) if strings.EqualFold(relationType, "basicUsers") || strings.EqualFold(relationType, "basicUser") { return "basic" } } } itemType := strings.TrimSpace(milestoneStringValue(user["type"])) if strings.EqualFold(itemType, "basicUsers") || strings.EqualFold(itemType, "basicUser") { return "basic" } return "ad" } func milestoneRoleUserDomainIsBasic(domain string) bool { domain = strings.TrimSpace(domain) domain = strings.TrimPrefix(domain, "[") domain = strings.TrimSuffix(domain, "]") return strings.EqualFold(domain, "basic") } func milestoneRoleUserSource(source string) string { source = strings.TrimSpace(strings.ToLower(source)) switch source { case "basic", "basicuser", "basicusers": return "basic" case "ad", "active-directory", "activedirectory", "directory": return "ad" default: return "" } } func milestoneRoleUserRelationType(source string) string { if milestoneRoleUserSource(source) == "basic" { return "basicUsers" } return "users" } func milestoneRoleUserDomainFromObject(user map[string]any) string { for _, key := range []string{ "domain", "domainName", "accountDomain", "windowsDomain", "netbiosDomain", "directoryDomain", } { if value := strings.TrimSpace(milestoneStringValue(user[key])); value != "" { return value } } return "" } func milestoneRoleIsAdministrators(role operationMilestoneRole) bool { for _, value := range []string{role.Name, role.DisplayName, operationMilestoneRoleDisplayName(role)} { if normalizeMilestoneRoleProtectionValue(value) == "administrators" { return true } } return false } func milestoneRoleUserItemIsHidden(user milestoneRoleUserItem) bool { candidates := []string{ milestoneProtectedUserQualifiedName(user.Domain, user.Username), milestoneProtectedUserQualifiedName(user.Domain, user.UserName), milestoneProtectedUserQualifiedName(user.Domain, user.Name), milestoneProtectedUserQualifiedName(user.Domain, user.DisplayName), } for _, candidate := range candidates { if milestoneProtectedUserNameIsHidden(candidate) { return true } } return false } func milestoneProtectedUserNameIsHidden(value string) bool { value = normalizeMilestoneRoleProtectionValue(value) switch value { case "nt-autorität\\netzwerkdienst", "nt authority\\network service", "tegadmin", "tegdssd\\tegadmin", "vordefiniert\\administratoren", "builtin\\administrators", "tegdssd\\milestone_administratoren": return true } domain, account, ok := strings.Cut(value, "\\") return ok && domain == "tegdssd" && strings.HasSuffix(account, "$") } func milestoneProtectedUserQualifiedName(domain string, account string) string { domain = strings.TrimSpace(domain) account = strings.TrimSpace(account) if account == "" { return "" } if domain == "" { return account } return domain + "\\" + account } func normalizeMilestoneRoleProtectionValue(value string) string { return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(value)), " ")) } func splitMilestoneDomainUser(value string) (string, string) { value = strings.TrimSpace(value) if value == "" { return "", "" } domain, username, ok := strings.Cut(value, "\\") if !ok || strings.TrimSpace(domain) == "" || strings.TrimSpace(username) == "" { return "", value } return strings.TrimSpace(domain), strings.TrimSpace(username) }