teg/backend/admin_milestone_roles.go
2026-06-26 16:31:26 +02:00

1032 lines
28 KiB
Go

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)
}