mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 02:28:59 +00:00
docs: fix Kubernetes DaemonSet deployment guide
Fixes #1091 - addresses all three documentation issues reported: 1. Binary path: Changed from /usr/local/bin/pulse-agent (which doesn't exist in the main image) to /opt/pulse/bin/pulse-agent-linux-amd64 2. PULSE_AGENT_ID: Added to example and documented why it's required for DaemonSets (prevents token conflicts when all pods share one API token) 3. Resource visibility flags: Added PULSE_KUBE_INCLUDE_ALL_PODS and PULSE_KUBE_INCLUDE_ALL_DEPLOYMENTS to example, with explanation of the default behavior (show only problematic resources) Also added tolerations, resource requests/limits, and ARM64 note.
This commit is contained in:
parent
607a303b77
commit
f527e6ebd0
10 changed files with 589 additions and 16 deletions
|
|
@ -89,7 +89,7 @@ spec:
|
|||
containers:
|
||||
- name: pulse-agent
|
||||
image: rcourtman/pulse:latest
|
||||
command: ["/usr/local/bin/pulse-agent"]
|
||||
command: ["/opt/pulse/bin/pulse-agent-linux-amd64"]
|
||||
args:
|
||||
- --enable-kubernetes
|
||||
env:
|
||||
|
|
@ -97,18 +97,70 @@ spec:
|
|||
value: "http://pulse-server.pulse.svc.cluster.local:7655"
|
||||
- name: PULSE_TOKEN
|
||||
value: "YOUR_API_TOKEN_HERE"
|
||||
- name: PULSE_AGENT_ID
|
||||
value: "my-k8s-cluster"
|
||||
- name: PULSE_ENABLE_HOST
|
||||
value: "false"
|
||||
- name: PULSE_KUBE_INCLUDE_ALL_PODS
|
||||
value: "true"
|
||||
- name: PULSE_KUBE_INCLUDE_ALL_DEPLOYMENTS
|
||||
value: "true"
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
resources: {}
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
```
|
||||
|
||||
> **Note for ARM64 clusters**: Replace `pulse-agent-linux-amd64` with `pulse-agent-linux-arm64`.
|
||||
|
||||
Use a token scoped for the agent:
|
||||
- `kubernetes:report` for Kubernetes reporting
|
||||
- `host-agent:report` if you enable host metrics
|
||||
|
||||
#### Important DaemonSet Configuration
|
||||
|
||||
**PULSE_AGENT_ID (Required for DaemonSets)**
|
||||
|
||||
When running as a DaemonSet, all pods share the same API token but need a unified identity. Without `PULSE_AGENT_ID`, each pod auto-generates a unique ID (e.g., `mac-xxxxx`), causing token conflicts:
|
||||
|
||||
```
|
||||
API token is already in use by agent "mac-aa5496fed726". Each Kubernetes agent must use a unique API token.
|
||||
```
|
||||
|
||||
Set `PULSE_AGENT_ID` to a shared cluster name so all pods report as one logical agent:
|
||||
|
||||
```yaml
|
||||
- name: PULSE_AGENT_ID
|
||||
value: "my-k8s-cluster"
|
||||
```
|
||||
|
||||
**Resource Visibility Flags**
|
||||
|
||||
By default, Pulse only shows resources with problems (unhealthy pods, failing deployments). To see all resources:
|
||||
|
||||
| Environment Variable | Description | Default |
|
||||
|---------------------|-------------|---------|
|
||||
| `PULSE_KUBE_INCLUDE_ALL_PODS` | Show all non-succeeded pods, not just problematic ones | `false` |
|
||||
| `PULSE_KUBE_INCLUDE_ALL_DEPLOYMENTS` | Show all deployments, not just those with issues | `false` |
|
||||
|
||||
For most monitoring use cases, set both to `true`:
|
||||
|
||||
```yaml
|
||||
- name: PULSE_KUBE_INCLUDE_ALL_PODS
|
||||
value: "true"
|
||||
- name: PULSE_KUBE_INCLUDE_ALL_DEPLOYMENTS
|
||||
value: "true"
|
||||
```
|
||||
|
||||
See [UNIFIED_AGENT.md](UNIFIED_AGENT.md) for all available configuration options.
|
||||
|
||||
#### Add Host Metrics (Optional)
|
||||
|
||||
If you want node CPU/memory/disk metrics, add privileged mode plus host mounts:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { Component, createSignal, Show, onMount, lazy, Suspense } from 'solid-js';
|
||||
import { Component, createSignal, Show, For, onMount, lazy, Suspense } from 'solid-js';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { apiClient, apiFetchJSON } from '@/utils/apiClient';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import Globe from 'lucide-solid/icons/globe';
|
||||
import Key from 'lucide-solid/icons/key';
|
||||
|
||||
const SetupWizard = lazy(() =>
|
||||
import('./SetupWizard').then((m) => ({ default: m.SetupWizard })),
|
||||
|
|
@ -13,7 +15,7 @@ interface LoginProps {
|
|||
securityStatus?: SecurityStatus; // Full security status from App.tsx to avoid redundant API call
|
||||
}
|
||||
|
||||
import type { SecurityStatus } from '@/types/config';
|
||||
import type { SecurityStatus, SSOProviderInfo } from '@/types/config';
|
||||
|
||||
export const Login: Component<LoginProps> = (props) => {
|
||||
const [username, setUsername] = createSignal('');
|
||||
|
|
@ -30,8 +32,10 @@ export const Login: Component<LoginProps> = (props) => {
|
|||
const [oidcMessage, setOidcMessage] = createSignal('');
|
||||
|
||||
const supportsOIDC = () => Boolean(authStatus()?.oidcEnabled);
|
||||
const ssoProviders = () => authStatus()?.ssoProviders || [];
|
||||
const hasMultipleProviders = () => ssoProviders().length > 0;
|
||||
|
||||
const resolveOidcError = (reason?: string | null) => {
|
||||
const resolveSSOError = (reason?: string | null) => {
|
||||
switch (reason) {
|
||||
case 'email_restricted':
|
||||
return 'Your account email is not permitted to access Pulse.';
|
||||
|
|
@ -51,6 +55,12 @@ export const Login: Component<LoginProps> = (props) => {
|
|||
return 'The identity provider is issuing HS256 tokens. Configure it to sign ID tokens with RS256 (see your IdP\'s OIDC settings).';
|
||||
case 'invalid_nonce':
|
||||
return 'Security validation failed (nonce mismatch). Please try again.';
|
||||
case 'saml_validation_failed':
|
||||
return 'SAML assertion validation failed. Please try again or contact an administrator.';
|
||||
case 'provider_not_found':
|
||||
return 'SSO provider not found or not configured.';
|
||||
case 'provider_not_initialized':
|
||||
return 'SSO provider is not properly configured. Contact your administrator.';
|
||||
default:
|
||||
return 'Single sign-on failed. Please try again or contact an administrator.';
|
||||
}
|
||||
|
|
@ -73,18 +83,35 @@ export const Login: Component<LoginProps> = (props) => {
|
|||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
// Handle OIDC callback
|
||||
const oidcStatus = params.get('oidc');
|
||||
if (oidcStatus === 'error') {
|
||||
const reason = params.get('oidc_error');
|
||||
setOidcError(resolveOidcError(reason));
|
||||
setOidcError(resolveSSOError(reason));
|
||||
setError('');
|
||||
} else if (oidcStatus === 'success') {
|
||||
setOidcMessage('Signed in successfully. Loading Pulse…');
|
||||
setError('');
|
||||
}
|
||||
if (oidcStatus) {
|
||||
|
||||
// Handle SAML callback
|
||||
const samlStatus = params.get('saml');
|
||||
if (samlStatus === 'error') {
|
||||
const reason = params.get('saml_error');
|
||||
setOidcError(resolveSSOError(reason));
|
||||
setError('');
|
||||
} else if (samlStatus === 'success') {
|
||||
setOidcMessage('Signed in successfully. Loading Pulse…');
|
||||
setError('');
|
||||
}
|
||||
|
||||
// Clean up URL parameters
|
||||
if (oidcStatus || samlStatus) {
|
||||
params.delete('oidc');
|
||||
params.delete('oidc_error');
|
||||
params.delete('saml');
|
||||
params.delete('saml_error');
|
||||
const newQuery = params.toString();
|
||||
const newUrl = `${window.location.pathname}${newQuery ? `?${newQuery}` : ''}`;
|
||||
window.history.replaceState({}, document.title, newUrl);
|
||||
|
|
@ -304,6 +331,7 @@ export const Login: Component<LoginProps> = (props) => {
|
|||
oidcError,
|
||||
oidcMessage,
|
||||
showLocalLogin: shouldShowLocalLogin(),
|
||||
ssoProviders: ssoProviders(),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
|
@ -344,6 +372,7 @@ const LoginForm: Component<{
|
|||
oidcError: () => string;
|
||||
oidcMessage: () => string;
|
||||
showLocalLogin: boolean;
|
||||
ssoProviders: SSOProviderInfo[];
|
||||
}> = (props) => {
|
||||
const {
|
||||
username,
|
||||
|
|
@ -361,6 +390,7 @@ const LoginForm: Component<{
|
|||
oidcError,
|
||||
oidcMessage,
|
||||
showLocalLogin,
|
||||
ssoProviders,
|
||||
} = props;
|
||||
|
||||
// Check if we're on the demo server
|
||||
|
|
@ -417,7 +447,67 @@ const LoginForm: Component<{
|
|||
class="mt-8 space-y-6 bg-white/80 dark:bg-gray-800/80 backdrop-blur-lg rounded-lg p-8 shadow-xl animate-slide-up"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Show when={supportsOIDC()}>
|
||||
{/* Multi-Provider SSO Section */}
|
||||
<Show when={ssoProviders.length > 0}>
|
||||
<div class="space-y-3">
|
||||
<For each={ssoProviders}>
|
||||
{(provider) => (
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full inline-flex items-center justify-center gap-2 px-4 py-3 rounded-lg border border-blue-500 text-blue-600 hover:bg-blue-50 transition dark:border-blue-400 dark:text-blue-200 dark:hover:bg-blue-900/40 ${oidcLoading() ? 'opacity-75 cursor-wait' : ''}`}
|
||||
disabled={oidcLoading()}
|
||||
onClick={() => {
|
||||
window.location.href = provider.loginUrl;
|
||||
}}
|
||||
>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<Show
|
||||
when={provider.iconUrl}
|
||||
fallback={
|
||||
provider.type === 'saml' ? (
|
||||
<Key class="w-5 h-5" />
|
||||
) : (
|
||||
<Globe class="w-5 h-5" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={provider.iconUrl}
|
||||
alt=""
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</Show>
|
||||
Continue with {provider.displayName || provider.name}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<Show when={oidcError()}>
|
||||
<div class="rounded-md bg-red-50 dark:bg-red-900/40 border border-red-200 dark:border-red-800 px-3 py-2 text-sm text-red-600 dark:text-red-300">
|
||||
{oidcError()}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={oidcMessage()}>
|
||||
<div class="rounded-md bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 px-3 py-2 text-sm text-green-600 dark:text-green-300">
|
||||
{oidcMessage()}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={showLocalLogin}>
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<span class="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-xs uppercase tracking-wide text-gray-400 dark:text-gray-500">
|
||||
or
|
||||
</span>
|
||||
<span class="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
|
||||
</div>
|
||||
<p class="text-xs text-center text-gray-500 dark:text-gray-400">
|
||||
Use your admin credentials to sign in below.
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
{/* Legacy OIDC fallback (when no multi-provider SSO but legacy OIDC is enabled) */}
|
||||
<Show when={ssoProviders.length === 0 && supportsOIDC()}>
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -435,14 +525,7 @@ const LoginForm: Component<{
|
|||
}
|
||||
>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.8"
|
||||
d="M21 12c0 4.97-4.03 9-9 9m9-9c0-4.97-4.03-9-9-9m9 9H3m9 9c-4.97 0-9-4.03-9-9m9 9c-1.5-1.35-3-4.5-3-9s1.5-7.65 3-9m0 18c1.5-1.35 3-4.5 3-9s-1.5-7.65-3-9"
|
||||
/>
|
||||
</svg>
|
||||
<Globe class="w-5 h-5" />
|
||||
Continue with Single Sign-On
|
||||
</span>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { ChangePasswordModal } from './ChangePasswordModal';
|
|||
import { UnifiedAgents } from './UnifiedAgents';
|
||||
import { AgentProfilesPanel } from './AgentProfilesPanel';
|
||||
import { OIDCPanel } from './OIDCPanel';
|
||||
import { SSOProvidersPanel } from './SSOProvidersPanel';
|
||||
import { AISettings } from './AISettings';
|
||||
import { AICostDashboard } from '@/components/AI/AICostDashboard';
|
||||
import { DiagnosticsPanel } from './DiagnosticsPanel';
|
||||
|
|
@ -3701,6 +3702,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
{/* Security Single Sign-On Tab */}
|
||||
<Show when={activeTab() === 'security-sso'}>
|
||||
<div class="space-y-6">
|
||||
<SSOProvidersPanel onConfigUpdated={loadSecurityStatus} />
|
||||
{/* Legacy OIDC panel for backward compatibility */}
|
||||
<OIDCPanel onConfigUpdated={loadSecurityStatus} />
|
||||
</div>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -136,10 +136,25 @@ export interface SecurityStatus {
|
|||
oidcIssuer?: string;
|
||||
oidcClientId?: string;
|
||||
oidcEnvOverrides?: Record<string, boolean>;
|
||||
oidcLogoutURL?: string;
|
||||
// Multi-provider SSO
|
||||
ssoProviders?: SSOProviderInfo[];
|
||||
// Token auth scopes (for kiosk/limited-access mode)
|
||||
tokenScopes?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* SSO provider info for login page
|
||||
*/
|
||||
export interface SSOProviderInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'oidc' | 'saml';
|
||||
displayName: string;
|
||||
iconUrl?: string;
|
||||
loginUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* First-run setup request
|
||||
*/
|
||||
|
|
|
|||
7
go.mod
7
go.mod
|
|
@ -7,6 +7,7 @@ toolchain go1.24.7
|
|||
require (
|
||||
github.com/IGLOU-EU/go-wildcard/v2 v2.1.0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/crewjam/saml v0.5.1
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/google/uuid v1.6.0
|
||||
|
|
@ -37,6 +38,7 @@ require (
|
|||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.4.21 // indirect
|
||||
github.com/beevik/etree v1.5.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
|
|
@ -58,19 +60,23 @@ require (
|
|||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-pdf/fpdf v0.9.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
|
|
@ -88,6 +94,7 @@ require (
|
|||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/russellhaering/goxmldsig v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
|
|
|
|||
26
go.sum
26
go.sum
|
|
@ -4,6 +4,9 @@ github.com/IGLOU-EU/go-wildcard/v2 v2.1.0 h1:WFqyYAuIYLJ6mHZ4rp/bYXiR4E1IvXW4+zI
|
|||
github.com/IGLOU-EU/go-wildcard/v2 v2.1.0/go.mod h1:/sUMQ5dk2owR0ZcjRI/4AZ+bUFF5DxGCQrDMNBXUf5o=
|
||||
github.com/Microsoft/go-winio v0.4.21 h1:+6mVbXh4wPzUrl1COX9A+ZCvEpYsOBZ6/+kwDnvLyro=
|
||||
github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs=
|
||||
github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
|
|
@ -21,6 +24,8 @@ github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSw
|
|||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/crewjam/saml v0.5.1 h1:g+mfp0CrLuLRZCK793PgJcZeg5dS/0CDwoeAX2zcwNI=
|
||||
github.com/crewjam/saml v0.5.1/go.mod h1:r0fDkmFe5URDgPrmtH0IYokva6fac3AUdstiPhyEolQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
|
|
@ -62,11 +67,15 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En
|
|||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
|
||||
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
|
|
@ -90,6 +99,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
|
|
@ -98,7 +109,9 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
|
|||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
|
|
@ -111,6 +124,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
|
|||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
|
|
@ -150,6 +165,7 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I
|
|||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
|
@ -167,6 +183,8 @@ github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4
|
|||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8=
|
||||
|
|
@ -174,6 +192,8 @@ github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1
|
|||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys=
|
||||
github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
|
|
@ -190,6 +210,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
|||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
|
|
@ -294,15 +315,20 @@ google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr
|
|||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE=
|
||||
|
|
|
|||
|
|
@ -310,3 +310,140 @@ func isPersistentLogger() bool {
|
|||
_, isConsole := logger.(*audit.ConsoleLogger)
|
||||
return !isConsole
|
||||
}
|
||||
|
||||
// HandleExportAuditEvents handles GET /api/audit/export
|
||||
func (h *AuditHandlers) HandleExportAuditEvents(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if persistent logger is available
|
||||
if !isPersistentLogger() {
|
||||
writeErrorResponse(w, http.StatusNotImplemented, "export_unavailable",
|
||||
"Export requires Pulse Pro with enterprise audit logging", nil)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
// Parse format
|
||||
format := audit.ExportFormatJSON
|
||||
if query.Get("format") == "csv" {
|
||||
format = audit.ExportFormatCSV
|
||||
}
|
||||
|
||||
// Parse filter
|
||||
filter := audit.QueryFilter{
|
||||
EventType: query.Get("event"),
|
||||
User: query.Get("user"),
|
||||
}
|
||||
|
||||
// Parse startTime
|
||||
if startStr := query.Get("startTime"); startStr != "" {
|
||||
if t, err := time.Parse(time.RFC3339, startStr); err == nil {
|
||||
filter.StartTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
// Parse endTime
|
||||
if endStr := query.Get("endTime"); endStr != "" {
|
||||
if t, err := time.Parse(time.RFC3339, endStr); err == nil {
|
||||
filter.EndTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
// Parse success
|
||||
if successStr := query.Get("success"); successStr != "" {
|
||||
success := successStr == "true"
|
||||
filter.Success = &success
|
||||
}
|
||||
|
||||
// Parse verification flag
|
||||
includeVerification := query.Get("verify") == "true"
|
||||
|
||||
// Get the SQLite logger
|
||||
logger := audit.GetLogger()
|
||||
sqliteLogger, ok := logger.(*audit.SQLiteLogger)
|
||||
if !ok {
|
||||
writeErrorResponse(w, http.StatusNotImplemented, "export_unavailable",
|
||||
"Export requires SQLite audit logger", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Create exporter and export
|
||||
exporter := audit.NewExporter(sqliteLogger)
|
||||
result, err := exporter.Export(filter, format, includeVerification)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, http.StatusInternalServerError, "export_failed",
|
||||
"Failed to export audit events", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Set response headers
|
||||
w.Header().Set("Content-Type", result.ContentType)
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", result.Filename))
|
||||
w.Header().Set("X-Event-Count", strconv.Itoa(result.EventCount))
|
||||
w.Write(result.Data)
|
||||
}
|
||||
|
||||
// HandleAuditSummary handles GET /api/audit/summary
|
||||
func (h *AuditHandlers) HandleAuditSummary(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if persistent logger is available
|
||||
if !isPersistentLogger() {
|
||||
writeErrorResponse(w, http.StatusNotImplemented, "summary_unavailable",
|
||||
"Summary requires Pulse Pro with enterprise audit logging", nil)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
// Parse filter
|
||||
filter := audit.QueryFilter{
|
||||
EventType: query.Get("event"),
|
||||
User: query.Get("user"),
|
||||
}
|
||||
|
||||
// Parse startTime
|
||||
if startStr := query.Get("startTime"); startStr != "" {
|
||||
if t, err := time.Parse(time.RFC3339, startStr); err == nil {
|
||||
filter.StartTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
// Parse endTime
|
||||
if endStr := query.Get("endTime"); endStr != "" {
|
||||
if t, err := time.Parse(time.RFC3339, endStr); err == nil {
|
||||
filter.EndTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
// Parse verification flag
|
||||
verifySignatures := query.Get("verify") == "true"
|
||||
|
||||
// Get the SQLite logger
|
||||
logger := audit.GetLogger()
|
||||
sqliteLogger, ok := logger.(*audit.SQLiteLogger)
|
||||
if !ok {
|
||||
writeErrorResponse(w, http.StatusNotImplemented, "summary_unavailable",
|
||||
"Summary requires SQLite audit logger", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Create exporter and generate summary
|
||||
exporter := audit.NewExporter(sqliteLogger)
|
||||
summary, err := exporter.GenerateSummary(filter, verifySignatures)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, http.StatusInternalServerError, "summary_failed",
|
||||
"Failed to generate audit summary", nil)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(summary)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ type Router struct {
|
|||
persistence *config.ConfigPersistence
|
||||
oidcMu sync.Mutex
|
||||
oidcService *OIDCService
|
||||
ssoConfig *config.SSOConfig
|
||||
samlManager *SAMLServiceManager
|
||||
authorizer auth.Authorizer
|
||||
wrapped http.Handler
|
||||
serverVersion string
|
||||
|
|
@ -133,6 +135,12 @@ func NewRouter(cfg *config.Config, monitor *monitoring.Monitor, wsHub *websocket
|
|||
updateManager := updates.NewManager(cfg)
|
||||
updateManager.SetHistory(updateHistory)
|
||||
|
||||
// Determine base URL for SSO callbacks
|
||||
baseURL := cfg.PublicURL
|
||||
if baseURL == "" {
|
||||
baseURL = fmt.Sprintf("http://localhost:%d", cfg.FrontendPort)
|
||||
}
|
||||
|
||||
r := &Router{
|
||||
mux: http.NewServeMux(),
|
||||
config: cfg,
|
||||
|
|
@ -148,6 +156,8 @@ func NewRouter(cfg *config.Config, monitor *monitoring.Monitor, wsHub *websocket
|
|||
serverVersion: strings.TrimSpace(serverVersion),
|
||||
projectRoot: projectRoot,
|
||||
checksumCache: make(map[string]checksumCacheEntry),
|
||||
ssoConfig: config.NewSSOConfig(),
|
||||
samlManager: NewSAMLServiceManager(baseURL),
|
||||
}
|
||||
|
||||
// Sync the configured admin user to the authorizer (if supported)
|
||||
|
|
@ -510,6 +520,8 @@ func (r *Router) setupRoutes() {
|
|||
r.mux.HandleFunc("GET /api/audit", RequirePermission(r.config, r.authorizer, auth.ActionRead, auth.ResourceAuditLogs, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAuditLogging, RequireScope(config.ScopeSettingsRead, auditHandlers.HandleListAuditEvents))))
|
||||
r.mux.HandleFunc("GET /api/audit/", RequirePermission(r.config, r.authorizer, auth.ActionRead, auth.ResourceAuditLogs, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAuditLogging, RequireScope(config.ScopeSettingsRead, auditHandlers.HandleListAuditEvents))))
|
||||
r.mux.HandleFunc("GET /api/audit/{id}/verify", RequirePermission(r.config, r.authorizer, auth.ActionRead, auth.ResourceAuditLogs, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAuditLogging, RequireScope(config.ScopeSettingsRead, auditHandlers.HandleVerifyAuditEvent))))
|
||||
r.mux.HandleFunc("GET /api/audit/export", RequirePermission(r.config, r.authorizer, auth.ActionRead, auth.ResourceAuditLogs, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAuditLogging, RequireScope(config.ScopeSettingsRead, auditHandlers.HandleExportAuditEvents))))
|
||||
r.mux.HandleFunc("GET /api/audit/summary", RequirePermission(r.config, r.authorizer, auth.ActionRead, auth.ResourceAuditLogs, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAuditLogging, RequireScope(config.ScopeSettingsRead, auditHandlers.HandleAuditSummary))))
|
||||
|
||||
// RBAC routes (Phase 2 - Enterprise feature)
|
||||
r.mux.HandleFunc("/api/admin/roles", RequirePermission(r.config, r.authorizer, auth.ActionAdmin, auth.ResourceUsers, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureRBAC, rbacHandlers.HandleRoles)))
|
||||
|
|
@ -537,6 +549,38 @@ func (r *Router) setupRoutes() {
|
|||
r.mux.HandleFunc("/api/security/oidc", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleOIDCConfig)))
|
||||
r.mux.HandleFunc("/api/oidc/login", r.handleOIDCLogin)
|
||||
r.mux.HandleFunc(config.DefaultOIDCCallbackPath, r.handleOIDCCallback)
|
||||
|
||||
// SAML SSO routes - dynamic provider-based endpoints
|
||||
r.mux.HandleFunc("/api/saml/", func(w http.ResponseWriter, req *http.Request) {
|
||||
// Route SAML requests based on path: /api/saml/{providerID}/{action}
|
||||
path := strings.TrimPrefix(req.URL.Path, "/api/saml/")
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
if len(parts) < 2 {
|
||||
writeErrorResponse(w, http.StatusBadRequest, "invalid_path", "Invalid SAML endpoint path", nil)
|
||||
return
|
||||
}
|
||||
action := parts[1]
|
||||
switch action {
|
||||
case "login":
|
||||
r.handleSAMLLogin(w, req)
|
||||
case "acs":
|
||||
r.handleSAMLACS(w, req)
|
||||
case "metadata":
|
||||
r.handleSAMLMetadata(w, req)
|
||||
case "logout":
|
||||
r.handleSAMLLogout(w, req)
|
||||
case "slo":
|
||||
r.handleSAMLSLO(w, req)
|
||||
default:
|
||||
writeErrorResponse(w, http.StatusNotFound, "not_found", "Unknown SAML endpoint", nil)
|
||||
}
|
||||
})
|
||||
|
||||
// SSO providers management API
|
||||
r.mux.HandleFunc("/api/security/sso/providers/test", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleTestSSOProvider)))
|
||||
r.mux.HandleFunc("/api/security/sso/providers/metadata/preview", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleMetadataPreview)))
|
||||
r.mux.HandleFunc("/api/security/sso/providers", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleSSOProviders)))
|
||||
r.mux.HandleFunc("/api/security/sso/providers/", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleSSOProvider)))
|
||||
r.mux.HandleFunc("/api/security/tokens", RequirePermission(r.config, r.authorizer, auth.ActionAdmin, auth.ResourceUsers, func(w http.ResponseWriter, req *http.Request) {
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
|
|
@ -708,6 +752,35 @@ func (r *Router) setupRoutes() {
|
|||
}
|
||||
}
|
||||
|
||||
// Add SSO providers for multi-provider support
|
||||
if r.ssoConfig != nil && len(r.ssoConfig.Providers) > 0 {
|
||||
enabledProviders := make([]map[string]interface{}, 0)
|
||||
for _, p := range r.ssoConfig.GetEnabledProviders() {
|
||||
provider := map[string]interface{}{
|
||||
"id": p.ID,
|
||||
"name": p.Name,
|
||||
"type": string(p.Type),
|
||||
"displayName": p.DisplayName,
|
||||
}
|
||||
if provider["displayName"] == "" {
|
||||
provider["displayName"] = p.Name
|
||||
}
|
||||
if p.IconURL != "" {
|
||||
provider["iconUrl"] = p.IconURL
|
||||
}
|
||||
// Add login URL for each provider
|
||||
if p.Type == config.SSOProviderTypeOIDC {
|
||||
provider["loginUrl"] = "/api/oidc/login?provider=" + p.ID
|
||||
} else if p.Type == config.SSOProviderTypeSAML {
|
||||
provider["loginUrl"] = "/api/saml/" + p.ID + "/login"
|
||||
}
|
||||
enabledProviders = append(enabledProviders, provider)
|
||||
}
|
||||
if len(enabledProviders) > 0 {
|
||||
status["ssoProviders"] = enabledProviders
|
||||
}
|
||||
}
|
||||
|
||||
// Add bootstrap token location for first-run setup UI
|
||||
if r.bootstrapTokenHash != "" {
|
||||
status["bootstrapTokenPath"] = r.bootstrapTokenPath
|
||||
|
|
@ -2068,6 +2141,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
"/api/login", // Add login endpoint as public
|
||||
"/api/oidc/login",
|
||||
config.DefaultOIDCCallbackPath,
|
||||
"/api/saml/", // All SAML endpoints are public (login, ACS, metadata, etc.)
|
||||
"/install-docker-agent.sh", // Docker agent bootstrap script must be public
|
||||
"/install-container-agent.sh", // Container agent bootstrap script must be public
|
||||
"/download/pulse-docker-agent", // Agent binary download should not require auth
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ type ConfigPersistence struct {
|
|||
nodesFile string
|
||||
systemFile string
|
||||
oidcFile string
|
||||
ssoFile string
|
||||
apiTokensFile string
|
||||
envTokenSuppressionsFile string
|
||||
aiFile string
|
||||
|
|
@ -108,6 +109,7 @@ func newConfigPersistence(configDir string) (*ConfigPersistence, error) {
|
|||
nodesFile: filepath.Join(configDir, "nodes.enc"),
|
||||
systemFile: filepath.Join(configDir, "system.json"),
|
||||
oidcFile: filepath.Join(configDir, "oidc.enc"),
|
||||
ssoFile: filepath.Join(configDir, "sso.enc"),
|
||||
apiTokensFile: filepath.Join(configDir, "api_tokens.json"),
|
||||
envTokenSuppressionsFile: filepath.Join(configDir, "env_token_suppressions.json"),
|
||||
aiFile: filepath.Join(configDir, "ai.enc"),
|
||||
|
|
@ -1473,6 +1475,147 @@ func (c *ConfigPersistence) LoadOIDCConfig() (*OIDCConfig, error) {
|
|||
return &settings, nil
|
||||
}
|
||||
|
||||
// SaveSSOConfig stores SSO settings, encrypting them when a crypto manager is available.
|
||||
func (c *ConfigPersistence) SaveSSOConfig(settings *SSOConfig) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if err := c.EnsureConfigDir(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
settings = NewSSOConfig()
|
||||
}
|
||||
|
||||
// Clone to avoid modifying the original
|
||||
clone := settings.Clone()
|
||||
|
||||
// Clear sensitive data from OIDC providers (env overrides)
|
||||
for i := range clone.Providers {
|
||||
if clone.Providers[i].OIDC != nil {
|
||||
clone.Providers[i].OIDC.EnvOverrides = nil
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(clone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.crypto != nil {
|
||||
encrypted, err := c.crypto.Encrypt(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = encrypted
|
||||
}
|
||||
|
||||
if err := c.writeConfigFileLocked(c.ssoFile, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Str("file", c.ssoFile).Int("providers", len(clone.Providers)).Msg("SSO configuration saved")
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadSSOConfig retrieves the persisted SSO settings. It returns nil when no configuration exists yet.
|
||||
// If no SSO config exists but legacy OIDC config does, it will automatically migrate the configuration.
|
||||
func (c *ConfigPersistence) LoadSSOConfig() (*SSOConfig, error) {
|
||||
c.mu.RLock()
|
||||
data, err := c.fs.ReadFile(c.ssoFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.mu.RUnlock()
|
||||
// Check if we should migrate from legacy OIDC config
|
||||
return c.migrateFromLegacyOIDCConfig()
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.crypto != nil {
|
||||
decrypted, err := c.crypto.Decrypt(data)
|
||||
if err != nil {
|
||||
c.mu.RUnlock()
|
||||
return nil, err
|
||||
}
|
||||
data = decrypted
|
||||
}
|
||||
|
||||
var settings SSOConfig
|
||||
if err := json.Unmarshal(data, &settings); err != nil {
|
||||
c.mu.RUnlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.mu.RUnlock()
|
||||
log.Info().Str("file", c.ssoFile).Int("providers", len(settings.Providers)).Msg("SSO configuration loaded")
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// migrateFromLegacyOIDCConfig checks for a legacy OIDC config file and migrates it to the new SSO format.
|
||||
// This function should be called WITHOUT any lock held.
|
||||
func (c *ConfigPersistence) migrateFromLegacyOIDCConfig() (*SSOConfig, error) {
|
||||
c.mu.RLock()
|
||||
// Check if legacy OIDC config exists
|
||||
data, err := c.fs.ReadFile(c.oidcFile)
|
||||
if err != nil {
|
||||
c.mu.RUnlock()
|
||||
if os.IsNotExist(err) {
|
||||
// No legacy config either, return nil
|
||||
return nil, nil
|
||||
}
|
||||
// Error reading file
|
||||
return nil, nil // Don't fail the startup, just return nil
|
||||
}
|
||||
|
||||
// Decrypt if needed
|
||||
if c.crypto != nil {
|
||||
decrypted, err := c.crypto.Decrypt(data)
|
||||
if err != nil {
|
||||
c.mu.RUnlock()
|
||||
log.Warn().Err(err).Msg("Failed to decrypt legacy OIDC config for migration")
|
||||
return nil, nil
|
||||
}
|
||||
data = decrypted
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
var oidcConfig OIDCConfig
|
||||
if err := json.Unmarshal(data, &oidcConfig); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to parse legacy OIDC config for migration")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Only migrate if OIDC was actually configured
|
||||
if oidcConfig.IssuerURL == "" || oidcConfig.ClientID == "" {
|
||||
log.Debug().Msg("Legacy OIDC config not configured, skipping migration")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Migrate to new SSO format
|
||||
ssoConfig := MigrateFromOIDCConfig(&oidcConfig)
|
||||
|
||||
log.Info().
|
||||
Str("issuer", oidcConfig.IssuerURL).
|
||||
Str("clientId", oidcConfig.ClientID).
|
||||
Bool("enabled", oidcConfig.Enabled).
|
||||
Msg("Migrating legacy OIDC configuration to new SSO format")
|
||||
|
||||
// Save the migrated config
|
||||
if err := c.SaveSSOConfig(ssoConfig); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save migrated SSO configuration")
|
||||
return ssoConfig, nil // Return the migrated config even if save failed
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Int("providers", len(ssoConfig.Providers)).
|
||||
Msg("Successfully migrated legacy OIDC config to new SSO format")
|
||||
|
||||
return ssoConfig, nil
|
||||
}
|
||||
|
||||
// SaveAIConfig stores AI settings, encrypting them when a crypto manager is available.
|
||||
func (c *ConfigPersistence) SaveAIConfig(settings AIConfig) error {
|
||||
c.mu.Lock()
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ import (
|
|||
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/api"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/crypto"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/license"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/logging"
|
||||
_ "github.com/rcourtman/pulse-go-rewrite/internal/mock" // Import for init() to run
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/audit"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -101,6 +103,37 @@ func Run(ctx context.Context, version string) error {
|
|||
log.Info().Msg("RBAC manager initialized")
|
||||
}
|
||||
|
||||
// Initialize SQLite audit logger for Pro/Enterprise
|
||||
// Check if audit logging feature is available via mock mode or license
|
||||
licenseService := license.NewService()
|
||||
if licenseService.HasFeature(license.FeatureAuditLogging) {
|
||||
// Initialize crypto manager for signing key encryption
|
||||
cryptoMgr, err := crypto.NewCryptoManagerAt(dataDir)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to initialize crypto manager for audit logging")
|
||||
} else {
|
||||
auditLogger, err := audit.NewSQLiteLogger(audit.SQLiteLoggerConfig{
|
||||
DataDir: dataDir,
|
||||
CryptoMgr: cryptoMgr,
|
||||
RetentionDays: 90, // Default 90 day retention
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to initialize SQLite audit logger, using console logger")
|
||||
} else {
|
||||
audit.SetLogger(auditLogger)
|
||||
log.Info().Msg("Enterprise audit logging enabled with SQLite backend")
|
||||
// Ensure cleanup on shutdown
|
||||
defer func() {
|
||||
if err := auditLogger.Close(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to close audit logger")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msg("Audit logging feature not licensed, using console logger")
|
||||
}
|
||||
|
||||
log.Info().Msg("Starting Pulse monitoring server")
|
||||
|
||||
// Validate agent binaries are available for download
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue