b20047b3eb
* feat(plugins): add auth capability for plugin-provided OIDC/SAML login Plugins can now authenticate a visitor against an external IdP (OIDC/SAML) and contribute a login button to the pre-auth screen: - new `auth` manifest capability + a capability-gated X-Kandev-Auth-Login webhook-response directive; the host mints the kandev_session (ADR 0050), the plugin never receives the raw token. Plugin-set Set-Cookie is dropped. - auth.Service.AuthenticateExternal: existing-identity / link-by-email / JIT member provisioning; refuses to auto-link to an admin; verified-email is the plugin's contract. - auth_providers manifest block + plugins.SSOProviders() populate the anonymous boot payload (auth.ssoProviders); the login page renders a "Continue with <provider>" button per provider. - ADR 0050 + plugin-manifest / plugin-authoring / auth spec docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: exclude publish_review_findings_kandev from public-docs coverage The native code-review agent (#1957) added the publish_review_findings_kandev MCP tool but did not update docs/public/coverage.json; that PR did not touch docs/public so the path-filtered validator never ran. It's an internal review reporting tool, not a user-facing feature, so record it as an exclusion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(plugins,auth): address review on SSO directive and JIT provisioning - takeAuthLoginDirective strips every case variant of X-Kandev-Auth-Login and rejects (403) when a plugin sends more than one, so no variant leaks to the browser and login never uses a nondeterministically-picked assertion. - provisionExternalUser rolls back the just-created user when the identity insert fails (transient error or a lost UNIQUE(provider,subject) race) and resolves to the winning identity, so no account is left without a login and no email is reserved by an unusable row. Adds userstore.DeleteUser. - auth spec: mark the login-page SSO buttons + provider discovery as landed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: drop redundant coverage exclusion; main now covers publish_review_findings_kandev Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
53 lines
1.8 KiB
Go
53 lines
1.8 KiB
Go
package plugins
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/kandev/kandev/internal/plugins/manifest"
|
|
"github.com/kandev/kandev/internal/plugins/store"
|
|
)
|
|
|
|
func ssoRecord(id string, status string, auth bool, webhooks []string, providers []manifest.AuthProvider) *store.Record {
|
|
whs := make([]manifest.Webhook, 0, len(webhooks))
|
|
for _, k := range webhooks {
|
|
whs = append(whs, manifest.Webhook{Key: k})
|
|
}
|
|
return &store.Record{
|
|
Status: status,
|
|
Manifest: manifest.Manifest{
|
|
ID: id,
|
|
Capabilities: manifest.Capabilities{Auth: auth},
|
|
Webhooks: whs,
|
|
AuthProviders: providers,
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestSSOProviders(t *testing.T) {
|
|
reg := NewRegistry()
|
|
// Valid: active, auth-capable, provider names a declared webhook.
|
|
reg.Add(ssoRecord("google", StatusActive, true, []string{"initiate", "callback"},
|
|
[]manifest.AuthProvider{{ID: "google", DisplayName: "Google", Initiate: "initiate"}}))
|
|
// Skipped: provider names an undeclared webhook (button would 404).
|
|
reg.Add(ssoRecord("broken", StatusActive, true, nil,
|
|
[]manifest.AuthProvider{{ID: "x", DisplayName: "X", Initiate: "missing"}}))
|
|
// Skipped: declares providers but lacks the auth capability.
|
|
reg.Add(ssoRecord("nocap", StatusActive, false, []string{"initiate"},
|
|
[]manifest.AuthProvider{{ID: "y", DisplayName: "Y", Initiate: "initiate"}}))
|
|
// Skipped: inactive plugin.
|
|
reg.Add(ssoRecord("inactive", store.StatusDisabled, true, []string{"initiate"},
|
|
[]manifest.AuthProvider{{ID: "z", DisplayName: "Z", Initiate: "initiate"}}))
|
|
|
|
svc := &Service{registry: reg}
|
|
got := svc.SSOProviders()
|
|
|
|
if len(got) != 1 {
|
|
t.Fatalf("SSOProviders() = %d providers, want 1: %+v", len(got), got)
|
|
}
|
|
p := got[0]
|
|
if p.ID != "google" || p.DisplayName != "Google" ||
|
|
p.InitiateURL != "/api/plugins/google/webhooks/initiate" {
|
|
t.Fatalf("unexpected provider: %+v", p)
|
|
}
|
|
}
|