package main import ( "embed" "encoding/json" "fmt" "html/template" "log" "net/http" "os" "strconv" "time" "github.com/go-ldap/ldap/v3" "github.com/gorilla/mux" "github.com/joho/godotenv" ) //go:embed auth.html var authFS embed.FS type Config struct { LDAPURL string BindDN string BindPassword string BaseDN string Port string } type AuthRequest struct { Username string `json:"username"` Password string `json:"password"` } type AuthResponse struct { Success bool `json:"success"` Username string `json:"username,omitempty"` FullName string `json:"full_name,omitempty"` Email string `json:"email,omitempty"` Groups []string `json:"groups,omitempty"` Description string `json:"description,omitempty"` AllInfo map[string]interface{} `json:"all_info,omitempty"` } type ErrorResponse struct { Success bool `json:"success"` Error string `json:"error"` } var ( config Config htmlTemplate = template.Must(template.ParseFS(authFS, "auth.html")) ) func init() { godotenv.Load() config = Config{ LDAPURL: getEnv("LDAP_URL", "ldap://dc.school25.ru:389"), BindDN: getEnv("LDAP_BIND_DN", "ldap@school25.ru"), BindPassword: getEnv("LDAP_BIND_PASSWORD", "password"), BaseDN: getEnv("LDAP_BASE_DN", "DC=school25,DC=ru"), Port: getEnv("PORT", "8080"), } } func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func testLDAPConnection() error { log.Println("Testing LDAP connection...") if config.LDAPURL == "" { return fmt.Errorf("LDAP_URL is required") } if config.BaseDN == "" { return fmt.Errorf("LDAP_BASE_DN is required") } l, err := ldap.DialURL(config.LDAPURL) if err != nil { return fmt.Errorf("failed to connect to LDAP server: %v", err) } defer l.Close() if config.BindDN != "" && config.BindPassword != "" { log.Printf("Testing bind with service account: %s", config.BindDN) err = l.Bind(config.BindDN, config.BindPassword) if err != nil { return fmt.Errorf("LDAP bind failed: %v", err) } log.Println("✓ Service account bind successful") searchRequest := ldap.NewSearchRequest( "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", []string{"defaultNamingContext"}, nil, ) sr, err := l.Search(searchRequest) if err != nil { log.Printf("⚠ Root DSE search failed: %v", err) } else if len(sr.Entries) > 0 { defaultNamingContext := sr.Entries[0].GetAttributeValue("defaultNamingContext") log.Printf("✓ Default naming context: %s", defaultNamingContext) } } else { log.Println("⚠ No service account credentials provided, using anonymous bind") err = l.Bind("", "") if err != nil { log.Printf("⚠ Anonymous bind failed: %v", err) } else { log.Println("✓ Anonymous bind successful") } } log.Printf("Testing BaseDN: %s", config.BaseDN) searchRequest := ldap.NewSearchRequest( config.BaseDN, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", []string{"distinguishedName"}, nil, ) _, err = l.Search(searchRequest) if err != nil { return fmt.Errorf("BaseDN test failed: %v - please check LDAP_BASE_DN configuration", err) } log.Println("✓ BaseDN validation successful") return nil } func testUserAuthentication() { testUser := os.Getenv("LDAP_TEST_USER") testPassword := os.Getenv("LDAP_TEST_PASSWORD") if testUser == "" || testPassword == "" { log.Println("⚠ No test user credentials provided, skipping user authentication test") return } log.Printf("Testing user authentication for: %s", testUser) _, err := authenticateLDAP(testUser, testPassword, false, false, false) if err != nil { log.Printf("⚠ User authentication test failed: %v", err) } else { log.Println("✓ User authentication test successful") } } func authenticateLDAP(username, password string, getAllInfo bool, descriptionOnly bool, groupsOnly bool) (*AuthResponse, error) { if username == "" { return nil, fmt.Errorf("username is required") } // Требуем пароль для всех пользователей if password == "" { return nil, fmt.Errorf("password is required") } l, err := ldap.DialURL(config.LDAPURL) if err != nil { return nil, fmt.Errorf("LDAP connection failed: %v", err) } defer l.Close() bindAttempts := []string{ fmt.Sprintf("%s@school25.ru", username), fmt.Sprintf("SCHOOL25\\%s", username), } var bindErr error for _, bindDN := range bindAttempts { err = l.Bind(bindDN, password) if err == nil { return getUserInfo(l, username, getAllInfo, descriptionOnly, groupsOnly) } bindErr = err log.Printf("Bind attempt failed for %s: %v", bindDN, err) } return nil, fmt.Errorf("authentication failed: %v", bindErr) } func getUserInfo(l *ldap.Conn, username string, getAllInfo bool, descriptionOnly bool, groupsOnly bool) (*AuthResponse, error) { if config.BindDN != "" && config.BindPassword != "" { err := l.Bind(config.BindDN, config.BindPassword) if err != nil { log.Printf("Warning: Service account rebind failed: %v", err) // Если не удалось перебиндиться, возвращаем только базовую информацию return &AuthResponse{ Success: true, Username: username, }, nil } } // Определяем какие атрибуты запрашивать в зависимости от режима attributes := []string{"dn", "sAMAccountName"} if !groupsOnly && !descriptionOnly { // Базовые атрибуты для обычного режима attributes = append(attributes, "memberOf", "mail", "displayName", "cn", "description") } if descriptionOnly { attributes = append(attributes, "description") } if groupsOnly { attributes = append(attributes, "memberOf") } if getAllInfo { // Запрашиваем все возможные атрибуты attributes = []string{"*", "+"} } searchRequest := ldap.NewSearchRequest( config.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, fmt.Sprintf("(sAMAccountName=%s)", ldap.EscapeFilter(username)), attributes, nil, ) sr, err := l.Search(searchRequest) if err != nil { return nil, fmt.Errorf("user search failed: %v", err) } if len(sr.Entries) == 0 { return nil, fmt.Errorf("user not found") } entry := sr.Entries[0] // Режим только description if descriptionOnly { description := entry.GetAttributeValue("description") return &AuthResponse{ Success: true, Description: description, }, nil } // Режим только groups if groupsOnly { groups := []string{} groupEntries := entry.GetAttributeValues("memberOf") for _, group := range groupEntries { if cn := extractCNFromDN(group); cn != "" { groups = append(groups, cn) } } return &AuthResponse{ Success: true, Groups: groups, }, nil } // Обычный режим или полная информация userInfo := &AuthResponse{ Success: true, Username: entry.GetAttributeValue("sAMAccountName"), FullName: entry.GetAttributeValue("displayName"), Email: entry.GetAttributeValue("mail"), Description: entry.GetAttributeValue("description"), Groups: []string{}, } if userInfo.FullName == "" { userInfo.FullName = entry.GetAttributeValue("cn") } if userInfo.FullName == "" { userInfo.FullName = username } // Извлекаем все группы groups := entry.GetAttributeValues("memberOf") for _, group := range groups { if cn := extractCNFromDN(group); cn != "" { userInfo.Groups = append(userInfo.Groups, cn) } } // Если запрошена полная информация, собираем все атрибуты if getAllInfo { userInfo.AllInfo = make(map[string]interface{}) for _, attr := range entry.Attributes { if len(attr.Values) == 1 { userInfo.AllInfo[attr.Name] = attr.Values[0] } else if len(attr.Values) > 1 { userInfo.AllInfo[attr.Name] = attr.Values } } } return userInfo, nil } func extractCNFromDN(dn string) string { rdn, err := ldap.ParseDN(dn) if err != nil || len(rdn.RDNs) == 0 { return "" } for _, attr := range rdn.RDNs[0].Attributes { if attr.Type == "CN" { return attr.Value } } return "" } func authHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) json.NewEncoder(w).Encode(ErrorResponse{Success: false, Error: "Method not allowed"}) return } var authReq AuthRequest if err := json.NewDecoder(r.Body).Decode(&authReq); err != nil { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(ErrorResponse{Success: false, Error: "Invalid JSON"}) return } if authReq.Username == "" { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(ErrorResponse{Success: false, Error: "Username is required"}) return } // Парсим параметры из query string queryParams := r.URL.Query() getAllInfo := false descriptionOnly := false groupsOnly := false if allParam := queryParams.Get("all"); allParam != "" { if allValue, err := strconv.Atoi(allParam); err == nil && allValue == 1 { getAllInfo = true } } if descParam := queryParams.Get("description"); descParam != "" { if descValue, err := strconv.Atoi(descParam); err == nil && descValue == 1 { descriptionOnly = true } } if groupsParam := queryParams.Get("groups"); groupsParam != "" { if groupsValue, err := strconv.Atoi(groupsParam); err == nil && groupsValue == 1 { groupsOnly = true } } // Проверяем конфликтующие параметры if (descriptionOnly && groupsOnly) || (descriptionOnly && getAllInfo) || (groupsOnly && getAllInfo) { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(ErrorResponse{Success: false, Error: "Conflicting parameters: use only one of description=1, groups=1, or all=1"}) return } authResponse, err := authenticateLDAP(authReq.Username, authReq.Password, getAllInfo, descriptionOnly, groupsOnly) if err != nil { log.Printf("Authentication failed for user %s: %v", authReq.Username, err) w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(ErrorResponse{Success: false, Error: err.Error()}) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(authResponse) } func healthHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") err := testLDAPConnection() if err != nil { w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(map[string]interface{}{ "status": "error", "error": err.Error(), }) return } json.NewEncoder(w).Encode(map[string]string{ "status": "ok", "ldap": "connected", }) } func webAuthHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := htmlTemplate.Execute(w, nil); err != nil { log.Printf("Error executing template: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } } func main() { log.Println("=== LDAP Auth Service Starting ===") log.Printf("LDAP URL: %s", config.LDAPURL) log.Printf("Base DN: %s", config.BaseDN) if config.BindDN != "" { log.Printf("Bind DN: %s", config.BindDN) } else { log.Printf("Bind DN: (anonymous)") } log.Printf("Port: %s", config.Port) log.Println("==================================") if err := testLDAPConnection(); err != nil { log.Fatalf("❌ LDAP connection test failed: %v", err) } log.Println("✅ LDAP connection test passed") testUserAuthentication() r := mux.NewRouter() r.HandleFunc("/api/auth", authHandler).Methods("POST", "OPTIONS") r.HandleFunc("/health", healthHandler).Methods("GET") r.HandleFunc("/web/auth", webAuthHandler).Methods("GET") r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/web/auth", http.StatusFound) }) r.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "status": "running", "service": "LDAP Auth", "ldap_url": config.LDAPURL, "base_dn": config.BaseDN, "timestamp": time.Now().Format(time.RFC3339), }) }).Methods("GET") log.Printf("🚀 Server starting on http://localhost:%s", config.Port) log.Printf("🌐 Web interface: http://localhost:%s/web/auth", config.Port) log.Printf("📊 Health check: http://localhost:%s/health", config.Port) log.Printf("🔌 REST API endpoints:") log.Printf(" Basic info: POST http://localhost:%s/api/auth", config.Port) log.Printf(" All info: POST http://localhost:%s/api/auth?all=1", config.Port) log.Printf(" Description: POST http://localhost:%s/api/auth?description=1", config.Port) log.Printf(" Groups only: POST http://localhost:%s/api/auth?groups=1", config.Port) if err := http.ListenAndServe(":"+config.Port, r); err != nil { log.Fatal(err) } }