Add files via upload
This commit is contained in:
497
auth.html
Normal file
497
auth.html
Normal file
@@ -0,0 +1,497 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>LDAP Authentication - school25.ru</title>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 15px 35px rgba(0,0,0,0.1);
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
}
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
input[type="text"]:focus, input[type="password"]:focus {
|
||||
border-color: #667eea;
|
||||
outline: none;
|
||||
}
|
||||
button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 15px 30px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
transition: transform 0.2s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.result {
|
||||
margin-top: 25px;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
display: none;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.user-info {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.user-info p {
|
||||
margin: 8px 0;
|
||||
color: #333;
|
||||
}
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
color: #667eea;
|
||||
margin: 10px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.test-users {
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
.test-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.test-button {
|
||||
background: #28a745;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.test-button:hover {
|
||||
background: #218838;
|
||||
}
|
||||
.api-info {
|
||||
margin-top: 25px;
|
||||
padding: 20px;
|
||||
background: #e9ecef;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.api-info h4 {
|
||||
margin-bottom: 15px;
|
||||
color: #495057;
|
||||
}
|
||||
.api-info code {
|
||||
background: #343a40;
|
||||
color: #f8f9fa;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: inline-block;
|
||||
margin: 2px 0;
|
||||
}
|
||||
.api-endpoints {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.endpoint {
|
||||
background: white;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
.mode-selector {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.mode-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.mode-option:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
.mode-option.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
.mode-option input[type="radio"] {
|
||||
margin: 0;
|
||||
}
|
||||
.json-preview {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-top: 10px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.password-warning {
|
||||
color: #dc3545;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>🔐 LDAP Authentication</h2>
|
||||
<p style="text-align: center; color: #666; margin-bottom: 25px;">school25.ru Domain - Password Required</p>
|
||||
|
||||
<form id="authForm">
|
||||
<div class="form-group">
|
||||
<label>Username:</label>
|
||||
<input type="text" id="username" name="username" placeholder="kalugin66" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password:</label>
|
||||
<input type="password" id="password" name="password" placeholder="••••••••" required>
|
||||
<div class="password-warning" id="passwordWarning">⚠ Password is required for all users</div>
|
||||
</div>
|
||||
|
||||
<div class="mode-selector">
|
||||
<label class="mode-option" id="modeBasic">
|
||||
<input type="radio" name="mode" value="basic" checked>
|
||||
<span>Basic Info</span>
|
||||
</label>
|
||||
<label class="mode-option" id="modeDescription">
|
||||
<input type="radio" name="mode" value="description">
|
||||
<span>Description Only</span>
|
||||
</label>
|
||||
<label class="mode-option" id="modeGroups">
|
||||
<input type="radio" name="mode" value="groups">
|
||||
<span>Groups Only</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mode-selector">
|
||||
<label class="mode-option" id="modeAll">
|
||||
<input type="radio" name="mode" value="all">
|
||||
<span>All Information</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Authenticate</button>
|
||||
</form>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
🔄 Authenticating with Active Directory...
|
||||
</div>
|
||||
|
||||
<div class="result" id="result">
|
||||
<!-- Результат будет здесь -->
|
||||
</div>
|
||||
|
||||
<div class="test-users">
|
||||
<h4>Quick Test Users:</h4>
|
||||
<div class="test-buttons">
|
||||
<button type="button" class="test-button" onclick="fillCredentials('kalugin66', 'Admin624600@')">
|
||||
kalugin66
|
||||
</button>
|
||||
<button type="button" class="test-button" onclick="fillCredentials('tv', 'Admin624600@')">
|
||||
tv (with password)
|
||||
</button>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: #666; margin-top: 8px;">
|
||||
Note: Password is now required for all users
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="api-info">
|
||||
<h4>📋 REST API Endpoints:</h4>
|
||||
|
||||
<div class="api-endpoints">
|
||||
<div class="endpoint">
|
||||
<strong>Basic Information:</strong><br>
|
||||
<code>POST /api/auth</code>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Description Only:</strong><br>
|
||||
<code>POST /api/auth?description=1</code>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Groups Only:</strong><br>
|
||||
<code>POST /api/auth?groups=1</code>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>All Information:</strong><br>
|
||||
<code>POST /api/auth?all=1</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>🔧 cURL Examples:</h4>
|
||||
<div class="json-preview" id="curlExamples">
|
||||
// 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"}'
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentMode = 'basic';
|
||||
|
||||
// Initialize mode selection
|
||||
document.querySelectorAll('.mode-option').forEach(option => {
|
||||
option.addEventListener('click', function() {
|
||||
document.querySelectorAll('.mode-option').forEach(opt => {
|
||||
opt.classList.remove('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
const radio = this.querySelector('input[type="radio"]');
|
||||
radio.checked = true;
|
||||
currentMode = radio.value;
|
||||
});
|
||||
});
|
||||
|
||||
// Set initial active state
|
||||
document.getElementById('modeBasic').classList.add('active');
|
||||
|
||||
function fillCredentials(username, password) {
|
||||
document.getElementById('username').value = username;
|
||||
document.getElementById('password').value = password;
|
||||
document.getElementById('passwordWarning').style.display = 'none';
|
||||
}
|
||||
|
||||
function showPasswordWarning() {
|
||||
document.getElementById('passwordWarning').style.display = 'block';
|
||||
}
|
||||
|
||||
function hidePasswordWarning() {
|
||||
document.getElementById('passwordWarning').style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('authForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const resultDiv = document.getElementById('result');
|
||||
const loadingDiv = document.getElementById('loading');
|
||||
|
||||
// Validate password
|
||||
if (!password) {
|
||||
showPasswordWarning();
|
||||
resultDiv.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
hidePasswordWarning();
|
||||
|
||||
// Показываем загрузку
|
||||
loadingDiv.style.display = 'block';
|
||||
resultDiv.style.display = 'none';
|
||||
|
||||
try {
|
||||
// Build URL based on selected mode
|
||||
let url = '/api/auth';
|
||||
switch(currentMode) {
|
||||
case 'description':
|
||||
url += '?description=1';
|
||||
break;
|
||||
case 'groups':
|
||||
url += '?groups=1';
|
||||
break;
|
||||
case 'all':
|
||||
url += '?all=1';
|
||||
break;
|
||||
default:
|
||||
// basic mode, no parameters
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
resultDiv.className = 'result success';
|
||||
|
||||
let resultHTML = `<h3>✅ Authentication Successful!</h3>`;
|
||||
|
||||
// Different display based on mode
|
||||
switch(currentMode) {
|
||||
case 'description':
|
||||
resultHTML += `
|
||||
<div class="user-info">
|
||||
<p><strong>Description:</strong> ${result.description || 'Not set'}</p>
|
||||
</div>
|
||||
<div class="json-preview">${JSON.stringify(result, null, 2)}</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
case 'groups':
|
||||
resultHTML += `
|
||||
<div class="user-info">
|
||||
<p><strong>Groups (${result.groups ? result.groups.length : 0}):</strong></p>
|
||||
<ul style="margin-left: 20px; margin-top: 8px;">
|
||||
${result.groups && result.groups.length > 0 ?
|
||||
result.groups.map(group => `<li>${group}</li>`).join('') :
|
||||
'<li>No groups found</li>'
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="json-preview">${JSON.stringify(result, null, 2)}</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
resultHTML += `
|
||||
<div class="user-info">
|
||||
<p><strong>Username:</strong> ${result.username || 'N/A'}</p>
|
||||
<p><strong>Full Name:</strong> ${result.full_name || 'N/A'}</p>
|
||||
<p><strong>Email:</strong> ${result.email || 'N/A'}</p>
|
||||
<p><strong>Description:</strong> ${result.description || 'N/A'}</p>
|
||||
<p><strong>Groups (${result.groups ? result.groups.length : 0}):</strong> ${result.groups && result.groups.length > 0 ? result.groups.join(', ') : 'No groups'}</p>
|
||||
</div>
|
||||
<h4 style="margin-top: 15px;">All LDAP Attributes:</h4>
|
||||
<div class="json-preview">${JSON.stringify(result.all_info, null, 2)}</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
default: // basic
|
||||
resultHTML += `
|
||||
<div class="user-info">
|
||||
<p><strong>Username:</strong> ${result.username || 'N/A'}</p>
|
||||
<p><strong>Full Name:</strong> ${result.full_name || 'N/A'}</p>
|
||||
<p><strong>Email:</strong> ${result.email || 'N/A'}</p>
|
||||
<p><strong>Description:</strong> ${result.description || 'Not set'}</p>
|
||||
<p><strong>Groups (${result.groups ? result.groups.length : 0}):</strong> ${result.groups && result.groups.length > 0 ? result.groups.join(', ') : 'No groups'}</p>
|
||||
</div>
|
||||
<div class="json-preview">${JSON.stringify(result, null, 2)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = resultHTML;
|
||||
} else {
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.innerHTML = `
|
||||
<h3>❌ Authentication Failed</h3>
|
||||
<p><strong>Error:</strong> ${result.error}</p>
|
||||
<div class="json-preview">${JSON.stringify(result, null, 2)}</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.innerHTML = `
|
||||
<h3>❌ Network Error</h3>
|
||||
<p><strong>Error:</strong> ${error.message}</p>
|
||||
<p>Please check if the server is running and try again.</p>
|
||||
`;
|
||||
} finally {
|
||||
loadingDiv.style.display = 'none';
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Password validation on input
|
||||
document.getElementById('password').addEventListener('input', function() {
|
||||
if (this.value) {
|
||||
hidePasswordWarning();
|
||||
}
|
||||
});
|
||||
|
||||
// Show password warning on focus if empty
|
||||
document.getElementById('password').addEventListener('focus', function() {
|
||||
if (!this.value) {
|
||||
showPasswordWarning();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
484
main.go
Normal file
484
main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user