From 719191dcab393be32fa587f923961d33002d4ffe Mon Sep 17 00:00:00 2001 From: kalugin66 <150135283+kalugin1988@users.noreply.github.com> Date: Fri, 17 Oct 2025 00:02:40 +0500 Subject: [PATCH] Add files via upload --- auth.html | 497 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 484 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 981 insertions(+) create mode 100644 auth.html create mode 100644 main.go diff --git a/auth.html b/auth.html new file mode 100644 index 0000000..51afdb0 --- /dev/null +++ b/auth.html @@ -0,0 +1,497 @@ + + + + LDAP Authentication - school25.ru + + + + +
+

🔐 LDAP Authentication

+

school25.ru Domain - Password Required

+ +
+
+ + +
+
+ + +
⚠ Password is required for all users
+
+ +
+ + + +
+ +
+ +
+ + +
+ +
+ 🔄 Authenticating with Active Directory... +
+ +
+ +
+ +
+

Quick Test Users:

+
+ + +
+

+ Note: Password is now required for all users +

+
+ +
+

📋 REST API Endpoints:

+ +
+
+ Basic Information:
+ POST /api/auth +
+ +
+ Description Only:
+ POST /api/auth?description=1 +
+ +
+ Groups Only:
+ POST /api/auth?groups=1 +
+ +
+ All Information:
+ POST /api/auth?all=1 +
+
+ +

🔧 cURL Examples:

+
+// Basic information +curl -X POST http://localhost:8080/api/auth \ + -H "Content-Type: application/json" \ + -d '{"username": "tv", "password": "password123"}' + +// Description only +curl -X POST "http://localhost:8080/api/auth?description=1" \ + -H "Content-Type: application/json" \ + -d '{"username": "tv", "password": "password123"}' + +// Groups only +curl -X POST "http://localhost:8080/api/auth?groups=1" \ + -H "Content-Type: application/json" \ + -d '{"username": "tv", "password": "password123"}' + +// All information +curl -X POST "http://localhost:8080/api/auth?all=1" \ + -H "Content-Type: application/json" \ + -d '{"username": "tv", "password": "password123"}' +
+
+
+ + + + \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..048c201 --- /dev/null +++ b/main.go @@ -0,0 +1,484 @@ +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) + } +}