1092 lines
28 KiB
Go
1092 lines
28 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-ldap/ldap/v3"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type activeDirectoryCredentials struct {
|
|
ServerURL string
|
|
BaseDN string
|
|
BindUsername string
|
|
BindPassword string
|
|
UserGroup string
|
|
SkipTLSVerify bool
|
|
}
|
|
|
|
type activeDirectoryUser struct {
|
|
ID string `json:"id"`
|
|
DisplayName string `json:"displayName"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
Domain string `json:"domain"`
|
|
DistinguishedName string `json:"distinguishedName"`
|
|
SID string `json:"sid"`
|
|
Source string `json:"source"`
|
|
}
|
|
|
|
type activeDirectorySettingsResponse struct {
|
|
ServerURL string `json:"serverUrl"`
|
|
BaseDN string `json:"baseDn"`
|
|
BindUsername string `json:"bindUsername"`
|
|
BindPasswordConfigured bool `json:"bindPasswordConfigured"`
|
|
UserGroup string `json:"userGroup"`
|
|
SkipTLSVerify bool `json:"skipTlsVerify"`
|
|
Configured bool `json:"configured"`
|
|
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
|
}
|
|
|
|
type updateActiveDirectorySettingsRequest struct {
|
|
ServerURL string `json:"serverUrl"`
|
|
BaseDN string `json:"baseDn"`
|
|
BindUsername string `json:"bindUsername"`
|
|
BindPassword string `json:"bindPassword"`
|
|
UserGroup string `json:"userGroup"`
|
|
SkipTLSVerify bool `json:"skipTlsVerify"`
|
|
}
|
|
|
|
type activeDirectoryBaseDNTreeRequest struct {
|
|
ServerURL string `json:"serverUrl"`
|
|
BindUsername string `json:"bindUsername"`
|
|
BindPassword string `json:"bindPassword"`
|
|
BaseDN string `json:"baseDn"`
|
|
SkipTLSVerify bool `json:"skipTlsVerify"`
|
|
}
|
|
|
|
type activeDirectoryBaseDNTreeNode struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
SecondaryName string `json:"secondaryName,omitempty"`
|
|
Children []activeDirectoryBaseDNTreeNode `json:"children,omitempty"`
|
|
Disabled bool `json:"disabled,omitempty"`
|
|
}
|
|
|
|
func (s *Server) handleGetActiveDirectorySettings(w http.ResponseWriter, r *http.Request) {
|
|
settings, err := s.getActiveDirectorySettings(r.Context())
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"settings": defaultActiveDirectorySettingsResponse(),
|
|
})
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "AD-Einstellungen konnten nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"settings": settings})
|
|
}
|
|
|
|
func (s *Server) handleUpdateActiveDirectorySettings(w http.ResponseWriter, r *http.Request) {
|
|
var input updateActiveDirectorySettingsRequest
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
serverURL := normalizeActiveDirectoryServerURL(input.ServerURL)
|
|
baseDN := strings.TrimSpace(input.BaseDN)
|
|
bindUsername := strings.TrimSpace(input.BindUsername)
|
|
bindPassword := strings.TrimSpace(input.BindPassword)
|
|
userGroup := strings.TrimSpace(input.UserGroup)
|
|
skipTLSVerify := input.SkipTLSVerify
|
|
|
|
currentSettings, err := s.getActiveDirectorySettings(r.Context())
|
|
passwordAlreadyConfigured := false
|
|
if err == nil {
|
|
passwordAlreadyConfigured = currentSettings.BindPasswordConfigured
|
|
} else if !errors.Is(err, pgx.ErrNoRows) {
|
|
writeError(w, http.StatusInternalServerError, "AD-Einstellungen konnten nicht geprueft werden")
|
|
return
|
|
}
|
|
|
|
configured := serverURL != "" || baseDN != "" || bindUsername != "" || bindPassword != ""
|
|
if configured {
|
|
if serverURL == "" {
|
|
writeError(w, http.StatusBadRequest, "AD-Server ist erforderlich")
|
|
return
|
|
}
|
|
if baseDN == "" {
|
|
writeError(w, http.StatusBadRequest, "AD-Base-DN ist erforderlich")
|
|
return
|
|
}
|
|
if bindUsername == "" {
|
|
writeError(w, http.StatusBadRequest, "AD-Bind-Benutzer ist erforderlich")
|
|
return
|
|
}
|
|
if bindPassword == "" && !passwordAlreadyConfigured {
|
|
writeError(w, http.StatusBadRequest, "AD-Bind-Kennwort ist erforderlich")
|
|
return
|
|
}
|
|
}
|
|
|
|
_, err = s.db.Exec(
|
|
r.Context(),
|
|
`
|
|
INSERT INTO milestone_settings (
|
|
id,
|
|
active_directory_server_url,
|
|
active_directory_base_dn,
|
|
active_directory_bind_username,
|
|
active_directory_bind_password_encrypted,
|
|
active_directory_user_group,
|
|
active_directory_skip_tls_verify
|
|
)
|
|
VALUES (
|
|
1,
|
|
$1,
|
|
$2,
|
|
$3,
|
|
CASE WHEN $4 <> '' THEN pgp_sym_encrypt($4, $5) ELSE NULL END,
|
|
$6,
|
|
$7
|
|
)
|
|
ON CONFLICT (id)
|
|
DO UPDATE SET
|
|
active_directory_server_url = EXCLUDED.active_directory_server_url,
|
|
active_directory_base_dn = EXCLUDED.active_directory_base_dn,
|
|
active_directory_bind_username = EXCLUDED.active_directory_bind_username,
|
|
active_directory_bind_password_encrypted = CASE
|
|
WHEN $1 = '' AND $2 = '' AND $3 = '' AND $4 = '' THEN NULL
|
|
WHEN $4 <> '' THEN EXCLUDED.active_directory_bind_password_encrypted
|
|
ELSE milestone_settings.active_directory_bind_password_encrypted
|
|
END,
|
|
active_directory_user_group = EXCLUDED.active_directory_user_group,
|
|
active_directory_skip_tls_verify = EXCLUDED.active_directory_skip_tls_verify,
|
|
updated_at = now()
|
|
`,
|
|
serverURL,
|
|
baseDN,
|
|
bindUsername,
|
|
bindPassword,
|
|
s.milestoneEncryptionKey(),
|
|
userGroup,
|
|
skipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "AD-Einstellungen konnten nicht gespeichert werden")
|
|
return
|
|
}
|
|
|
|
settings, err := s.getActiveDirectorySettings(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "AD-Einstellungen wurden gespeichert, konnten aber nicht neu geladen werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"settings": settings})
|
|
}
|
|
|
|
func (s *Server) handleListActiveDirectoryBaseDNTree(w http.ResponseWriter, r *http.Request) {
|
|
var input activeDirectoryBaseDNTreeRequest
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
credentials, ok := s.activeDirectoryBrowseCredentials(w, r, input)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
nodes, err := listActiveDirectoryBaseDNTree(credentials)
|
|
if err != nil {
|
|
logError("AD-Base-DN-Struktur konnte nicht geladen werden", err, LogFields{
|
|
"serverUrl": credentials.ServerURL,
|
|
})
|
|
writeError(w, http.StatusBadGateway, "AD-Base-DN-Struktur konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"nodes": nodes,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleListActiveDirectoryGroupTree(w http.ResponseWriter, r *http.Request) {
|
|
var input activeDirectoryBaseDNTreeRequest
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
credentials, ok := s.activeDirectoryBrowseCredentials(w, r, input)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
nodes, err := listActiveDirectoryGroupTree(credentials)
|
|
if err != nil {
|
|
logError("AD-Gruppenstruktur konnte nicht geladen werden", err, LogFields{
|
|
"serverUrl": credentials.ServerURL,
|
|
"baseDn": credentials.BaseDN,
|
|
})
|
|
writeError(w, http.StatusBadGateway, "AD-Gruppenstruktur konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"nodes": nodes,
|
|
})
|
|
}
|
|
|
|
func (s *Server) activeDirectoryBrowseCredentials(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
input activeDirectoryBaseDNTreeRequest,
|
|
) (activeDirectoryCredentials, bool) {
|
|
credentials := activeDirectoryCredentials{
|
|
ServerURL: normalizeActiveDirectoryServerURL(input.ServerURL),
|
|
BaseDN: strings.TrimSpace(input.BaseDN),
|
|
BindUsername: strings.TrimSpace(input.BindUsername),
|
|
BindPassword: strings.TrimSpace(input.BindPassword),
|
|
SkipTLSVerify: input.SkipTLSVerify,
|
|
}
|
|
|
|
storedCredentials, err := s.getActiveDirectoryCredentials(r.Context())
|
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
|
writeError(w, http.StatusInternalServerError, "AD-Einstellungen konnten nicht geladen werden")
|
|
return credentials, false
|
|
}
|
|
|
|
if credentials.ServerURL == "" {
|
|
credentials.ServerURL = storedCredentials.ServerURL
|
|
}
|
|
if credentials.BindUsername == "" {
|
|
credentials.BindUsername = storedCredentials.BindUsername
|
|
}
|
|
if credentials.BindPassword == "" {
|
|
credentials.BindPassword = storedCredentials.BindPassword
|
|
}
|
|
if credentials.BaseDN == "" {
|
|
credentials.BaseDN = storedCredentials.BaseDN
|
|
}
|
|
|
|
if credentials.ServerURL == "" || credentials.BindUsername == "" || credentials.BindPassword == "" {
|
|
writeError(w, http.StatusBadRequest, "AD-Server, Bind-Benutzer und Bind-Kennwort sind erforderlich")
|
|
return credentials, false
|
|
}
|
|
|
|
return credentials, true
|
|
}
|
|
|
|
func (s *Server) handleListOperationActiveDirectoryUsers(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 defaultActiveDirectorySettingsResponse() activeDirectorySettingsResponse {
|
|
settings := activeDirectorySettingsResponse{}
|
|
settings.Configured = settings.BindPasswordConfigured &&
|
|
settings.ServerURL != "" &&
|
|
settings.BaseDN != "" &&
|
|
settings.BindUsername != ""
|
|
|
|
return settings
|
|
}
|
|
|
|
func (s *Server) getActiveDirectorySettings(ctx context.Context) (activeDirectorySettingsResponse, error) {
|
|
var settings activeDirectorySettingsResponse
|
|
var updatedAt time.Time
|
|
|
|
err := s.db.QueryRow(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
COALESCE(active_directory_server_url, ''),
|
|
COALESCE(active_directory_base_dn, ''),
|
|
COALESCE(active_directory_bind_username, ''),
|
|
active_directory_bind_password_encrypted IS NOT NULL,
|
|
COALESCE(active_directory_user_group, ''),
|
|
COALESCE(active_directory_skip_tls_verify, false),
|
|
updated_at
|
|
FROM milestone_settings
|
|
WHERE id = 1
|
|
LIMIT 1
|
|
`,
|
|
).Scan(
|
|
&settings.ServerURL,
|
|
&settings.BaseDN,
|
|
&settings.BindUsername,
|
|
&settings.BindPasswordConfigured,
|
|
&settings.UserGroup,
|
|
&settings.SkipTLSVerify,
|
|
&updatedAt,
|
|
)
|
|
if err != nil {
|
|
return settings, err
|
|
}
|
|
|
|
settings.UpdatedAt = &updatedAt
|
|
settings.Configured = settings.BindPasswordConfigured &&
|
|
settings.ServerURL != "" &&
|
|
settings.BaseDN != "" &&
|
|
settings.BindUsername != ""
|
|
|
|
return settings, nil
|
|
}
|
|
|
|
func (c activeDirectoryCredentials) Configured() bool {
|
|
return strings.TrimSpace(c.ServerURL) != "" &&
|
|
strings.TrimSpace(c.BaseDN) != "" &&
|
|
strings.TrimSpace(c.BindUsername) != "" &&
|
|
strings.TrimSpace(c.BindPassword) != ""
|
|
}
|
|
|
|
func (s *Server) getActiveDirectoryCredentials(ctx context.Context) (activeDirectoryCredentials, error) {
|
|
var credentials activeDirectoryCredentials
|
|
|
|
err := s.db.QueryRow(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
COALESCE(active_directory_server_url, ''),
|
|
COALESCE(active_directory_base_dn, ''),
|
|
COALESCE(active_directory_bind_username, ''),
|
|
COALESCE(pgp_sym_decrypt(active_directory_bind_password_encrypted, $1), ''),
|
|
COALESCE(active_directory_user_group, ''),
|
|
COALESCE(active_directory_skip_tls_verify, false)
|
|
FROM milestone_settings
|
|
WHERE id = 1
|
|
LIMIT 1
|
|
`,
|
|
s.milestoneEncryptionKey(),
|
|
).Scan(
|
|
&credentials.ServerURL,
|
|
&credentials.BaseDN,
|
|
&credentials.BindUsername,
|
|
&credentials.BindPassword,
|
|
&credentials.UserGroup,
|
|
&credentials.SkipTLSVerify,
|
|
)
|
|
|
|
credentials.ServerURL = normalizeActiveDirectoryServerURL(credentials.ServerURL)
|
|
credentials.BaseDN = strings.TrimSpace(credentials.BaseDN)
|
|
credentials.BindUsername = strings.TrimSpace(credentials.BindUsername)
|
|
credentials.BindPassword = strings.TrimSpace(credentials.BindPassword)
|
|
credentials.UserGroup = strings.TrimSpace(credentials.UserGroup)
|
|
|
|
return credentials, err
|
|
}
|
|
|
|
func listActiveDirectoryUsers(
|
|
ctx context.Context,
|
|
credentials activeDirectoryCredentials,
|
|
) ([]activeDirectoryUser, error) {
|
|
conn, err := dialAndBindActiveDirectory(credentials)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
searchRequest := ldap.NewSearchRequest(
|
|
credentials.BaseDN,
|
|
ldap.ScopeWholeSubtree,
|
|
ldap.NeverDerefAliases,
|
|
0,
|
|
20,
|
|
false,
|
|
"(&"+
|
|
"(objectCategory=person)"+
|
|
"(objectClass=user)"+
|
|
"(!(userAccountControl:1.2.840.113556.1.4.803:=2))"+
|
|
")",
|
|
[]string{
|
|
"displayName",
|
|
"sAMAccountName",
|
|
"userPrincipalName",
|
|
"mail",
|
|
"distinguishedName",
|
|
"objectSid",
|
|
"cn",
|
|
},
|
|
nil,
|
|
)
|
|
|
|
searchResult, err := conn.Search(searchRequest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
users := make([]activeDirectoryUser, 0, len(searchResult.Entries))
|
|
seen := map[string]bool{}
|
|
for _, entry := range searchResult.Entries {
|
|
user := activeDirectoryUserFromLDAPEntry(entry)
|
|
if user.ID == "" {
|
|
continue
|
|
}
|
|
if seen[user.ID] {
|
|
continue
|
|
}
|
|
|
|
seen[user.ID] = true
|
|
users = append(users, user)
|
|
}
|
|
|
|
sort.SliceStable(users, func(i, j int) bool {
|
|
return strings.ToLower(users[i].DisplayName) < strings.ToLower(users[j].DisplayName)
|
|
})
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func dialAndBindActiveDirectory(credentials activeDirectoryCredentials) (*ldap.Conn, error) {
|
|
conn, err := ldap.DialURL(
|
|
credentials.ServerURL,
|
|
ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: credentials.SkipTLSVerify}),
|
|
ldap.DialWithDialer(&net.Dialer{Timeout: 15 * time.Second}),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := conn.Bind(credentials.BindUsername, credentials.BindPassword); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
func listActiveDirectoryBaseDNTree(
|
|
credentials activeDirectoryCredentials,
|
|
) ([]activeDirectoryBaseDNTreeNode, error) {
|
|
conn, err := dialAndBindActiveDirectory(credentials)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
namingContexts, err := listActiveDirectoryNamingContexts(conn, credentials.BaseDN)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
const maxNodes = 500
|
|
const maxDepth = 3
|
|
remainingNodes := maxNodes
|
|
nodes := make([]activeDirectoryBaseDNTreeNode, 0, len(namingContexts))
|
|
for _, namingContext := range namingContexts {
|
|
node := activeDirectoryBaseDNTreeNodeFromDN(namingContext)
|
|
if remainingNodes > 0 {
|
|
children, err := listActiveDirectoryBaseDNChildren(
|
|
conn,
|
|
namingContext,
|
|
0,
|
|
maxDepth,
|
|
&remainingNodes,
|
|
)
|
|
if err == nil {
|
|
node.Children = children
|
|
} else {
|
|
logError("AD-Base-DN-Unterstruktur konnte nicht geladen werden", err, LogFields{
|
|
"baseDn": namingContext,
|
|
})
|
|
}
|
|
}
|
|
|
|
nodes = append(nodes, node)
|
|
}
|
|
|
|
return nodes, nil
|
|
}
|
|
|
|
func listActiveDirectoryGroupTree(
|
|
credentials activeDirectoryCredentials,
|
|
) ([]activeDirectoryBaseDNTreeNode, error) {
|
|
conn, err := dialAndBindActiveDirectory(credentials)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
roots := []string{}
|
|
if strings.TrimSpace(credentials.BaseDN) != "" {
|
|
roots = append(roots, strings.TrimSpace(credentials.BaseDN))
|
|
} else {
|
|
roots, err = listActiveDirectoryNamingContexts(conn, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
const maxNodes = 1000
|
|
const maxDepth = 5
|
|
remainingNodes := maxNodes
|
|
nodes := make([]activeDirectoryBaseDNTreeNode, 0, len(roots))
|
|
for _, root := range roots {
|
|
node := activeDirectoryBaseDNTreeNodeFromDN(root)
|
|
node.Disabled = true
|
|
if remainingNodes > 0 {
|
|
children, err := listActiveDirectoryGroupTreeChildren(
|
|
conn,
|
|
root,
|
|
0,
|
|
maxDepth,
|
|
&remainingNodes,
|
|
)
|
|
if err == nil {
|
|
node.Children = children
|
|
} else {
|
|
logError("AD-Gruppen-Unterstruktur konnte nicht geladen werden", err, LogFields{
|
|
"baseDn": root,
|
|
})
|
|
}
|
|
}
|
|
|
|
nodes = append(nodes, node)
|
|
}
|
|
|
|
return nodes, nil
|
|
}
|
|
|
|
func listActiveDirectoryNamingContexts(conn *ldap.Conn, fallbackBaseDN string) ([]string, error) {
|
|
searchRequest := ldap.NewSearchRequest(
|
|
"",
|
|
ldap.ScopeBaseObject,
|
|
ldap.NeverDerefAliases,
|
|
0,
|
|
10,
|
|
false,
|
|
"(objectClass=*)",
|
|
[]string{"defaultNamingContext", "namingContexts"},
|
|
nil,
|
|
)
|
|
|
|
searchResult, err := conn.Search(searchRequest)
|
|
if err != nil {
|
|
if strings.TrimSpace(fallbackBaseDN) != "" {
|
|
return []string{strings.TrimSpace(fallbackBaseDN)}, nil
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
seen := map[string]bool{}
|
|
namingContexts := []string{}
|
|
addNamingContext := func(value string) {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || seen[strings.ToLower(value)] {
|
|
return
|
|
}
|
|
|
|
seen[strings.ToLower(value)] = true
|
|
namingContexts = append(namingContexts, value)
|
|
}
|
|
|
|
if len(searchResult.Entries) > 0 {
|
|
entry := searchResult.Entries[0]
|
|
addNamingContext(entry.GetAttributeValue("defaultNamingContext"))
|
|
for _, namingContext := range entry.GetAttributeValues("namingContexts") {
|
|
addNamingContext(namingContext)
|
|
}
|
|
}
|
|
|
|
addNamingContext(fallbackBaseDN)
|
|
|
|
if len(namingContexts) == 0 {
|
|
return nil, fmt.Errorf("keine AD-Naming-Contexts gefunden")
|
|
}
|
|
|
|
return namingContexts, nil
|
|
}
|
|
|
|
func listActiveDirectoryBaseDNChildren(
|
|
conn *ldap.Conn,
|
|
parentDN string,
|
|
depth int,
|
|
maxDepth int,
|
|
remainingNodes *int,
|
|
) ([]activeDirectoryBaseDNTreeNode, error) {
|
|
if depth >= maxDepth || *remainingNodes <= 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
searchRequest := ldap.NewSearchRequest(
|
|
parentDN,
|
|
ldap.ScopeSingleLevel,
|
|
ldap.NeverDerefAliases,
|
|
0,
|
|
10,
|
|
false,
|
|
"(|(objectClass=domainDNS)(objectClass=organizationalUnit)(objectClass=container))",
|
|
[]string{
|
|
"cn",
|
|
"distinguishedName",
|
|
"name",
|
|
"objectClass",
|
|
"ou",
|
|
},
|
|
nil,
|
|
)
|
|
|
|
searchResult, err := conn.Search(searchRequest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nodes := make([]activeDirectoryBaseDNTreeNode, 0, len(searchResult.Entries))
|
|
for _, entry := range searchResult.Entries {
|
|
if *remainingNodes <= 0 {
|
|
break
|
|
}
|
|
|
|
distinguishedName := strings.TrimSpace(entry.GetAttributeValue("distinguishedName"))
|
|
if distinguishedName == "" {
|
|
distinguishedName = strings.TrimSpace(entry.DN)
|
|
}
|
|
if distinguishedName == "" {
|
|
continue
|
|
}
|
|
|
|
*remainingNodes = *remainingNodes - 1
|
|
node := activeDirectoryBaseDNTreeNodeFromEntry(entry, distinguishedName)
|
|
children, err := listActiveDirectoryBaseDNChildren(
|
|
conn,
|
|
distinguishedName,
|
|
depth+1,
|
|
maxDepth,
|
|
remainingNodes,
|
|
)
|
|
if err == nil {
|
|
node.Children = children
|
|
} else {
|
|
logError("AD-Base-DN-Unterstruktur konnte nicht geladen werden", err, LogFields{
|
|
"baseDn": distinguishedName,
|
|
})
|
|
}
|
|
|
|
nodes = append(nodes, node)
|
|
}
|
|
|
|
sort.SliceStable(nodes, func(i, j int) bool {
|
|
return strings.ToLower(nodes[i].Name) < strings.ToLower(nodes[j].Name)
|
|
})
|
|
|
|
return nodes, nil
|
|
}
|
|
|
|
func listActiveDirectoryGroupTreeChildren(
|
|
conn *ldap.Conn,
|
|
parentDN string,
|
|
depth int,
|
|
maxDepth int,
|
|
remainingNodes *int,
|
|
) ([]activeDirectoryBaseDNTreeNode, error) {
|
|
if depth >= maxDepth || *remainingNodes <= 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
searchRequest := ldap.NewSearchRequest(
|
|
parentDN,
|
|
ldap.ScopeSingleLevel,
|
|
ldap.NeverDerefAliases,
|
|
0,
|
|
15,
|
|
false,
|
|
"(|(objectClass=domainDNS)(objectClass=organizationalUnit)(objectClass=container)(objectClass=group))",
|
|
[]string{
|
|
"cn",
|
|
"distinguishedName",
|
|
"name",
|
|
"objectClass",
|
|
"ou",
|
|
"sAMAccountName",
|
|
},
|
|
nil,
|
|
)
|
|
|
|
searchResult, err := conn.Search(searchRequest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nodes := make([]activeDirectoryBaseDNTreeNode, 0, len(searchResult.Entries))
|
|
for _, entry := range searchResult.Entries {
|
|
if *remainingNodes <= 0 {
|
|
break
|
|
}
|
|
|
|
distinguishedName := strings.TrimSpace(entry.GetAttributeValue("distinguishedName"))
|
|
if distinguishedName == "" {
|
|
distinguishedName = strings.TrimSpace(entry.DN)
|
|
}
|
|
if distinguishedName == "" {
|
|
continue
|
|
}
|
|
|
|
*remainingNodes = *remainingNodes - 1
|
|
isGroup := activeDirectoryEntryHasObjectClass(entry, "group")
|
|
node := activeDirectoryBaseDNTreeNodeFromEntry(entry, distinguishedName)
|
|
node.Disabled = !isGroup
|
|
if isGroup {
|
|
samAccountName := strings.TrimSpace(entry.GetAttributeValue("sAMAccountName"))
|
|
if samAccountName != "" {
|
|
node.SecondaryName = samAccountName
|
|
}
|
|
}
|
|
|
|
if !isGroup {
|
|
children, err := listActiveDirectoryGroupTreeChildren(
|
|
conn,
|
|
distinguishedName,
|
|
depth+1,
|
|
maxDepth,
|
|
remainingNodes,
|
|
)
|
|
if err == nil {
|
|
node.Children = children
|
|
} else {
|
|
logError("AD-Gruppen-Unterstruktur konnte nicht geladen werden", err, LogFields{
|
|
"baseDn": distinguishedName,
|
|
})
|
|
}
|
|
}
|
|
|
|
if isGroup || len(node.Children) > 0 {
|
|
nodes = append(nodes, node)
|
|
}
|
|
}
|
|
|
|
sort.SliceStable(nodes, func(i, j int) bool {
|
|
if nodes[i].Disabled != nodes[j].Disabled {
|
|
return !nodes[i].Disabled
|
|
}
|
|
|
|
return strings.ToLower(nodes[i].Name) < strings.ToLower(nodes[j].Name)
|
|
})
|
|
|
|
return nodes, nil
|
|
}
|
|
|
|
func activeDirectoryEntryHasObjectClass(entry *ldap.Entry, objectClass string) bool {
|
|
for _, value := range entry.GetAttributeValues("objectClass") {
|
|
if strings.EqualFold(value, objectClass) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func activeDirectoryBaseDNTreeNodeFromEntry(
|
|
entry *ldap.Entry,
|
|
distinguishedName string,
|
|
) activeDirectoryBaseDNTreeNode {
|
|
name := strings.TrimSpace(entry.GetAttributeValue("ou"))
|
|
if name == "" {
|
|
name = strings.TrimSpace(entry.GetAttributeValue("name"))
|
|
}
|
|
if name == "" {
|
|
name = strings.TrimSpace(entry.GetAttributeValue("cn"))
|
|
}
|
|
if name == "" {
|
|
name = activeDirectoryDNDisplayName(distinguishedName)
|
|
}
|
|
|
|
return activeDirectoryBaseDNTreeNode{
|
|
ID: distinguishedName,
|
|
Name: name,
|
|
Path: distinguishedName,
|
|
SecondaryName: distinguishedName,
|
|
}
|
|
}
|
|
|
|
func activeDirectoryBaseDNTreeNodeFromDN(distinguishedName string) activeDirectoryBaseDNTreeNode {
|
|
return activeDirectoryBaseDNTreeNode{
|
|
ID: distinguishedName,
|
|
Name: activeDirectoryDNDisplayName(distinguishedName),
|
|
Path: distinguishedName,
|
|
SecondaryName: distinguishedName,
|
|
}
|
|
}
|
|
|
|
func activeDirectoryDNDisplayName(distinguishedName string) string {
|
|
components := splitActiveDirectoryDN(distinguishedName)
|
|
if len(components) == 0 {
|
|
return distinguishedName
|
|
}
|
|
|
|
domainParts := make([]string, 0, len(components))
|
|
allDomainParts := true
|
|
for _, component := range components {
|
|
attribute, value := activeDirectoryDNRDN(component)
|
|
if !strings.EqualFold(attribute, "DC") {
|
|
allDomainParts = false
|
|
break
|
|
}
|
|
|
|
domainParts = append(domainParts, value)
|
|
}
|
|
if allDomainParts && len(domainParts) > 0 {
|
|
return strings.Join(domainParts, ".")
|
|
}
|
|
|
|
_, value := activeDirectoryDNRDN(components[0])
|
|
if value != "" {
|
|
return value
|
|
}
|
|
|
|
return distinguishedName
|
|
}
|
|
|
|
func splitActiveDirectoryDN(distinguishedName string) []string {
|
|
components := []string{}
|
|
var current strings.Builder
|
|
escaped := false
|
|
|
|
for _, char := range distinguishedName {
|
|
if escaped {
|
|
current.WriteRune(char)
|
|
escaped = false
|
|
continue
|
|
}
|
|
|
|
if char == '\\' {
|
|
escaped = true
|
|
current.WriteRune(char)
|
|
continue
|
|
}
|
|
|
|
if char == ',' {
|
|
component := strings.TrimSpace(current.String())
|
|
if component != "" {
|
|
components = append(components, component)
|
|
}
|
|
current.Reset()
|
|
continue
|
|
}
|
|
|
|
current.WriteRune(char)
|
|
}
|
|
|
|
component := strings.TrimSpace(current.String())
|
|
if component != "" {
|
|
components = append(components, component)
|
|
}
|
|
|
|
return components
|
|
}
|
|
|
|
func activeDirectoryDNRDN(component string) (string, string) {
|
|
parts := strings.SplitN(component, "=", 2)
|
|
if len(parts) != 2 {
|
|
return "", strings.TrimSpace(component)
|
|
}
|
|
|
|
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
|
}
|
|
|
|
func findActiveDirectoryGroupDN(
|
|
conn *ldap.Conn,
|
|
credentials activeDirectoryCredentials,
|
|
) (string, error) {
|
|
group := strings.TrimSpace(credentials.UserGroup)
|
|
if strings.Contains(group, "=") && strings.Contains(group, ",") {
|
|
return group, nil
|
|
}
|
|
|
|
escapedGroup := ldap.EscapeFilter(group)
|
|
groupFilter := fmt.Sprintf(
|
|
"(&(objectClass=group)(|(cn=%s)(name=%s)(sAMAccountName=%s)))",
|
|
escapedGroup,
|
|
escapedGroup,
|
|
escapedGroup,
|
|
)
|
|
searchRequest := ldap.NewSearchRequest(
|
|
credentials.BaseDN,
|
|
ldap.ScopeWholeSubtree,
|
|
ldap.NeverDerefAliases,
|
|
1,
|
|
10,
|
|
false,
|
|
groupFilter,
|
|
[]string{"distinguishedName", "cn", "name", "sAMAccountName"},
|
|
nil,
|
|
)
|
|
|
|
searchResult, err := conn.Search(searchRequest)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(searchResult.Entries) == 0 {
|
|
return "", fmt.Errorf("AD-Gruppe %q wurde nicht gefunden", group)
|
|
}
|
|
|
|
return searchResult.Entries[0].DN, nil
|
|
}
|
|
|
|
func activeDirectoryUserFromLDAPEntry(entry *ldap.Entry) activeDirectoryUser {
|
|
displayName := strings.TrimSpace(entry.GetAttributeValue("displayName"))
|
|
username := strings.TrimSpace(entry.GetAttributeValue("sAMAccountName"))
|
|
email := strings.TrimSpace(entry.GetAttributeValue("mail"))
|
|
userPrincipalName := strings.TrimSpace(entry.GetAttributeValue("userPrincipalName"))
|
|
distinguishedName := strings.TrimSpace(entry.GetAttributeValue("distinguishedName"))
|
|
if distinguishedName == "" {
|
|
distinguishedName = strings.TrimSpace(entry.DN)
|
|
}
|
|
if email == "" {
|
|
email = userPrincipalName
|
|
}
|
|
if displayName == "" {
|
|
displayName = strings.TrimSpace(entry.GetAttributeValue("cn"))
|
|
}
|
|
if displayName == "" {
|
|
displayName = username
|
|
}
|
|
if displayName == "" {
|
|
displayName = email
|
|
}
|
|
|
|
sid := activeDirectorySIDString(entry.GetRawAttributeValue("objectSid"))
|
|
id := sid
|
|
if id == "" {
|
|
id = distinguishedName
|
|
}
|
|
domain := activeDirectoryDomainFromPrincipal(userPrincipalName)
|
|
if domain == "" {
|
|
domain = activeDirectoryDomainFromDN(distinguishedName)
|
|
}
|
|
|
|
return activeDirectoryUser{
|
|
ID: id,
|
|
DisplayName: displayName,
|
|
Username: username,
|
|
Email: email,
|
|
Domain: domain,
|
|
DistinguishedName: distinguishedName,
|
|
SID: sid,
|
|
Source: "ad",
|
|
}
|
|
}
|
|
|
|
func activeDirectoryDomainFromPrincipal(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
|
|
_, domain, ok := strings.Cut(value, "@")
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
return strings.TrimSpace(domain)
|
|
}
|
|
|
|
func activeDirectoryDomainFromDN(distinguishedName string) string {
|
|
components := splitActiveDirectoryDN(distinguishedName)
|
|
if len(components) == 0 {
|
|
return ""
|
|
}
|
|
|
|
domainParts := make([]string, 0, len(components))
|
|
for _, component := range components {
|
|
attribute, value := activeDirectoryDNRDN(component)
|
|
if strings.EqualFold(attribute, "DC") && value != "" {
|
|
domainParts = append(domainParts, value)
|
|
}
|
|
}
|
|
|
|
return strings.Join(domainParts, ".")
|
|
}
|
|
|
|
func activeDirectorySIDString(rawSID []byte) string {
|
|
if len(rawSID) < 8 {
|
|
return ""
|
|
}
|
|
|
|
revision := rawSID[0]
|
|
subAuthorityCount := int(rawSID[1])
|
|
expectedLength := 8 + subAuthorityCount*4
|
|
if len(rawSID) < expectedLength {
|
|
return ""
|
|
}
|
|
|
|
var identifierAuthority uint64
|
|
for _, value := range rawSID[2:8] {
|
|
identifierAuthority = (identifierAuthority << 8) | uint64(value)
|
|
}
|
|
|
|
parts := []string{
|
|
"S",
|
|
fmt.Sprintf("%d", revision),
|
|
fmt.Sprintf("%d", identifierAuthority),
|
|
}
|
|
for index := 0; index < subAuthorityCount; index++ {
|
|
offset := 8 + index*4
|
|
parts = append(parts, fmt.Sprintf("%d", binary.LittleEndian.Uint32(rawSID[offset:offset+4])))
|
|
}
|
|
|
|
return strings.Join(parts, "-")
|
|
}
|
|
|
|
func normalizeActiveDirectoryServerURL(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
if !strings.Contains(value, "://") {
|
|
value = "ldaps://" + value
|
|
}
|
|
|
|
parsedURL, err := url.Parse(value)
|
|
if err != nil || parsedURL.Hostname() == "" {
|
|
return value
|
|
}
|
|
if parsedURL.Port() == "" {
|
|
if parsedURL.Scheme == "ldap" {
|
|
parsedURL.Host = parsedURL.Hostname() + ":389"
|
|
} else if parsedURL.Scheme == "ldaps" {
|
|
parsedURL.Host = parsedURL.Hostname() + ":636"
|
|
}
|
|
}
|
|
|
|
return parsedURL.String()
|
|
}
|