Add files via upload

This commit is contained in:
kalugin66
2025-10-17 00:02:40 +05:00
committed by GitHub
commit 719191dcab
2 changed files with 981 additions and 0 deletions

484
main.go Normal file
View File

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