sso
This commit is contained in:
491
internal/handlers/test.go
Normal file
491
internal/handlers/test.go
Normal file
@ -0,0 +1,491 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TestHandler handles test endpoints for development
|
||||
type TestHandler struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTestHandler creates a new test handler
|
||||
func NewTestHandler(logger *zap.Logger) *TestHandler {
|
||||
return &TestHandler{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SSOTestPage serves the SSO manual test page
|
||||
func (h *TestHandler) SSOTestPage(c *gin.Context) {
|
||||
h.logger.Debug("Serving SSO test page")
|
||||
|
||||
html := `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KMS SSO Manual Testing</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||
.container { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
h1 { color: #333; border-bottom: 3px solid #007acc; padding-bottom: 10px; }
|
||||
h2 { color: #007acc; margin-top: 30px; }
|
||||
.test-section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; background: #fafafa; }
|
||||
.btn { display: inline-block; padding: 12px 20px; margin: 10px 5px; background: #007acc; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; }
|
||||
.btn:hover { background: #005a9e; }
|
||||
.btn-secondary { background: #28a745; }
|
||||
.btn-warning { background: #ffc107; color: #333; }
|
||||
.status { padding: 10px; margin: 10px 0; border-radius: 3px; }
|
||||
.status.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
|
||||
.status.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
|
||||
.status.info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
|
||||
.code { background: #f8f9fa; padding: 15px; border-radius: 5px; font-family: monospace; margin: 10px 0; overflow-x: auto; }
|
||||
.endpoint { margin: 10px 0; }
|
||||
.endpoint strong { color: #007acc; }
|
||||
pre { background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 20px 0; }
|
||||
.card { padding: 20px; border: 1px solid #ddd; border-radius: 5px; background: white; }
|
||||
.user-info { background: #e7f3ff; padding: 10px; border-radius: 5px; margin: 10px 0; }
|
||||
button { background: #007acc; color: white; border: none; padding: 12px 20px; border-radius: 5px; cursor: pointer; font-weight: bold; margin: 5px; }
|
||||
button:hover { background: #005a9e; }
|
||||
.test-result { margin: 10px 0; padding: 10px; border-radius: 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 KMS SSO Manual Testing Suite</h1>
|
||||
<p><strong>Served from KMS API</strong> - No CORS issues!</p>
|
||||
|
||||
<div class="status info">
|
||||
<strong>Environment Status:</strong> Local Development
|
||||
<div id="services-status">Loading service status...</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>🎯 OAuth2/OIDC Testing (Keycloak)</h2>
|
||||
|
||||
<div class="user-info">
|
||||
<strong>Test Users:</strong><br>
|
||||
• admin@example.com / admin123 (Full access)<br>
|
||||
• test@example.com / test123 (Limited access)<br>
|
||||
• limited@example.com / limited123 (Read-only)
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Admin Console:</strong><br>
|
||||
<a href="http://localhost:8090" target="_blank" class="btn">Open Keycloak Admin</a>
|
||||
<small>Login: admin / admin</small>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>OAuth2 Authorization Flow:</strong><br>
|
||||
<a href="http://localhost:8090/realms/kms/protocol/openid-connect/auth?client_id=kms-api&response_type=code&redirect_uri=http://localhost:3000/callback&scope=openid+email+profile&state=test123" target="_blank" class="btn">Test OAuth2 Login</a>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Discovery Document:</strong><br>
|
||||
<button onclick="testOIDCDiscovery()">Test OIDC Discovery</button>
|
||||
<div id="oidc-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>📝 SAML Testing (SimpleSAMLphp)</h2>
|
||||
|
||||
<div class="user-info">
|
||||
<strong>Test Users:</strong><br>
|
||||
• user1 / user1pass<br>
|
||||
• user2 / user2pass
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Admin Console:</strong><br>
|
||||
<a href="http://localhost:8091/simplesaml" target="_blank" class="btn">Open SAML Admin</a>
|
||||
<small>Login: admin / secret</small>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>SAML Metadata:</strong><br>
|
||||
<button onclick="testSAMLMetadata()">Test SAML Metadata</button>
|
||||
<div id="saml-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Test Authentication:</strong><br>
|
||||
<a href="http://localhost:8091/simplesaml/module.php/core/authenticate.php?as=example-userpass" target="_blank" class="btn">Test SAML Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🚀 KMS API Testing</h2>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Frontend Application:</strong><br>
|
||||
<a href="http://localhost:3000" target="_blank" class="btn">Open KMS Frontend</a>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>API Health Check:</strong><br>
|
||||
<button onclick="testHealth()">Check API Health</button>
|
||||
<div id="health-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Test API with Different Users:</strong><br>
|
||||
<button onclick="testAPI('admin@example.com')">Test as Admin</button>
|
||||
<button onclick="testAPI('test@example.com')">Test as User</button>
|
||||
<button onclick="testAPI('limited@example.com')">Test as Limited</button>
|
||||
<div id="api-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Create Test Application:</strong><br>
|
||||
<button onclick="createTestApp()">Create Test App</button>
|
||||
<div id="create-app-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🔍 Permission System Testing</h2>
|
||||
<button onclick="testPermissions()">Test Permission System</button>
|
||||
<div id="permission-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>📊 Current Implementation Status</h2>
|
||||
|
||||
<div class="status success">
|
||||
<strong>✅ Working:</strong><br>
|
||||
• Keycloak OAuth2/OIDC provider with test realm<br>
|
||||
• SimpleSAMLphp SAML IdP with test users<br>
|
||||
• KMS API with header authentication<br>
|
||||
• Hierarchical permission system (25+ permissions)<br>
|
||||
• Application and token management<br>
|
||||
• Database with proper permission structure
|
||||
</div>
|
||||
|
||||
<div class="status error">
|
||||
<strong>❌ Missing:</strong><br>
|
||||
• OAuth2 callback handler in KMS API<br>
|
||||
• SAML assertion processing in KMS API<br>
|
||||
• Frontend SSO login integration<br>
|
||||
• Automatic permission mapping from SSO claims
|
||||
</div>
|
||||
|
||||
<div class="status info">
|
||||
<strong>ℹ️ Next Steps:</strong><br>
|
||||
• Complete OAuth2 callback implementation<br>
|
||||
• Add SAML response handling<br>
|
||||
• Map SSO user attributes to KMS permissions<br>
|
||||
• Add SSO login buttons to frontend
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🛠️ Development Commands</h2>
|
||||
<div class="code">
|
||||
<pre># Start SSO services
|
||||
podman-compose -f docker-compose.yml -f docker-compose.sso.yml up -d
|
||||
|
||||
# Run automated tests
|
||||
./test/quick_sso_test.sh
|
||||
|
||||
# Check service logs
|
||||
podman-compose logs keycloak
|
||||
podman-compose logs saml-idp
|
||||
podman-compose logs api-service
|
||||
|
||||
# Reset to header auth mode
|
||||
podman-compose up -d</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Test OIDC Discovery
|
||||
async function testOIDCDiscovery() {
|
||||
const resultDiv = document.getElementById('oidc-result');
|
||||
resultDiv.innerHTML = '<div style="color: blue;">Testing OIDC discovery...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8090/realms/kms/.well-known/openid-configuration');
|
||||
const data = await response.json();
|
||||
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
|
||||
<strong>✅ OIDC Discovery Successful!</strong><br>
|
||||
<strong>Authorization Endpoint:</strong> ${data.authorization_endpoint}<br>
|
||||
<strong>Token Endpoint:</strong> ${data.token_endpoint}<br>
|
||||
<strong>UserInfo Endpoint:</strong> ${data.userinfo_endpoint}<br>
|
||||
<details style="margin-top: 10px;">
|
||||
<summary>View Full Response</summary>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">${JSON.stringify(data, null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
` + "`" + `;
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ OIDC Discovery Failed</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
// Test SAML Metadata
|
||||
async function testSAMLMetadata() {
|
||||
const resultDiv = document.getElementById('saml-result');
|
||||
resultDiv.innerHTML = '<div style="color: blue;">Testing SAML metadata...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8091/simplesaml/saml2/idp/metadata.php');
|
||||
const text = await response.text();
|
||||
|
||||
if (text.includes('EntityDescriptor')) {
|
||||
const entityId = text.match(/entityID="([^"]*)"/)?.[1] || 'Not found';
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
|
||||
<strong>✅ SAML Metadata Accessible!</strong><br>
|
||||
<strong>Entity ID:</strong> ${entityId}<br>
|
||||
<details style="margin-top: 10px;">
|
||||
<summary>View Metadata</summary>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">${text}</pre>
|
||||
</details>
|
||||
</div>
|
||||
` + "`" + `;
|
||||
} else {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ Invalid SAML Metadata</strong><br>
|
||||
Response does not contain EntityDescriptor
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ SAML Metadata Test Failed</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
// Test API Health
|
||||
async function testHealth() {
|
||||
const resultDiv = document.getElementById('health-result');
|
||||
resultDiv.innerHTML = '<div style="color: blue;">Checking API health...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/health');
|
||||
const text = await response.text();
|
||||
|
||||
if (text === 'healthy') {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
|
||||
<strong>✅ API is Healthy!</strong>
|
||||
</div>
|
||||
` + "`" + `;
|
||||
} else {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ API Health Check Failed</strong><br>
|
||||
Response: ${text}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ API Health Check Error</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
// Test API with different users (simulating SSO result)
|
||||
async function testAPI(userEmail) {
|
||||
const resultDiv = document.getElementById('api-result');
|
||||
resultDiv.innerHTML = ` + "`" + `<div style="color: blue;">Testing API as ${userEmail}...</div>` + "`" + `;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/applications', {
|
||||
headers: {
|
||||
'X-User-Email': userEmail,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
|
||||
<strong>✅ API Test Successful (${userEmail})!</strong><br>
|
||||
Found ${data.count} applications<br>
|
||||
<details style="margin-top: 10px;">
|
||||
<summary>View Response</summary>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">${JSON.stringify(data, null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
` + "`" + `;
|
||||
} else {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ API Test Failed (${userEmail})</strong><br>
|
||||
Status: ${response.status} ${response.statusText}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ API Test Error (${userEmail})</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
// Create test application
|
||||
async function createTestApp() {
|
||||
const resultDiv = document.getElementById('create-app-result');
|
||||
resultDiv.innerHTML = '<div style="color: blue;">Creating test application...</div>';
|
||||
|
||||
const testAppData = {
|
||||
app_id: ` + "`" + `sso-test-${Date.now()}` + "`" + `,
|
||||
app_link: "https://test.example.com",
|
||||
type: ["static"],
|
||||
callback_url: "https://test.example.com/callback",
|
||||
token_prefix: "TEST",
|
||||
token_renewal_duration: 604800000000000,
|
||||
max_token_duration: 2592000000000000,
|
||||
owner: {
|
||||
type: "individual",
|
||||
name: "SSO Test App",
|
||||
owner: "admin@example.com"
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/applications', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-User-Email': 'admin@example.com',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(testAppData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
|
||||
<strong>✅ Application Created Successfully!</strong><br>
|
||||
<strong>App ID:</strong> ${data.app_id}<br>
|
||||
<strong>HMAC Key:</strong> ${data.hmac_key.substring(0, 20)}...<br>
|
||||
<details style="margin-top: 10px;">
|
||||
<summary>View Full Response</summary>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">${JSON.stringify(data, null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
` + "`" + `;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ Application Creation Failed</strong><br>
|
||||
Status: ${response.status} ${response.statusText}<br>
|
||||
${errorText}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ Application Creation Error</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
// Test permission system
|
||||
async function testPermissions() {
|
||||
const resultDiv = document.getElementById('permission-result');
|
||||
resultDiv.innerHTML = '<div style="color: blue;">Testing permission system...</div>';
|
||||
|
||||
// Test different permission levels by trying to access different endpoints
|
||||
const tests = [
|
||||
{ user: 'admin@example.com', description: 'Admin (full access)' },
|
||||
{ user: 'test@example.com', description: 'Test user (limited access)' },
|
||||
{ user: 'limited@example.com', description: 'Limited user (read-only)' }
|
||||
];
|
||||
|
||||
let results = '<div class="test-result" style="background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460;"><strong>Permission Test Results:</strong><br><br>';
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
const response = await fetch('/api/applications', {
|
||||
headers: {
|
||||
'X-User-Email': test.user,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
results += ` + "`" + `<span style="color: #28a745;">✅ ${test.description}: Can access applications (${data.count} found)</span><br>` + "`" + `;
|
||||
} else {
|
||||
results += ` + "`" + `<span style="color: #dc3545;">❌ ${test.description}: Access denied (${response.status})</span><br>` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
results += ` + "`" + `<span style="color: #dc3545;">❌ ${test.description}: Error - ${error.message}</span><br>` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
results += '</div>';
|
||||
resultDiv.innerHTML = results;
|
||||
}
|
||||
|
||||
// Check service status on load
|
||||
window.addEventListener('load', async function() {
|
||||
const statusDiv = document.getElementById('services-status');
|
||||
statusDiv.innerHTML = '<br>Checking services...';
|
||||
|
||||
const services = [
|
||||
{ name: 'KMS API', test: () => fetch('/health') },
|
||||
{ name: 'Keycloak', test: () => fetch('http://localhost:8090') },
|
||||
{ name: 'SAML IdP', test: () => fetch('http://localhost:8091/simplesaml') }
|
||||
];
|
||||
|
||||
let statusHtml = '<br>';
|
||||
|
||||
for (const service of services) {
|
||||
try {
|
||||
const response = await service.test();
|
||||
if (response.ok) {
|
||||
statusHtml += ` + "`" + `<span style="color: green;">✅ ${service.name}: Online</span><br>` + "`" + `;
|
||||
} else {
|
||||
statusHtml += ` + "`" + `<span style="color: orange;">⚠️ ${service.name}: Response ${response.status}</span><br>` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
statusHtml += ` + "`" + `<span style="color: red;">❌ ${service.name}: Not accessible</span><br>` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
statusDiv.innerHTML = statusHtml;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
|
||||
}
|
||||
Reference in New Issue
Block a user