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 Domain - Password Required
+
+
+
+
+ 🔄 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)
+ }
+}