Files
skybridge/internal/handlers/test.go
2025-08-26 19:15:37 -04:00

491 lines
22 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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