Files
wanderer/db/pluginsystem/auth_injection_test.go
slothful-vassal 485ec53f6d feat: add plugin system (#1034)
* feat: add plugin system

* fix db docker build

* fix hammerhead readme, add strava subscription news to docs

* fixes and sdk improvements

* fix: reduce Meilisearch load, debounce federation sync (#1012)

* optimize meili trail index

* several fixes

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>

* Bump svelte from 5.55.5 to 5.56.0 in /docs (#1032)

Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.55.5 to 5.56.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.56.0/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.56.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>

* Release v0.19.2 (#1035)

* chore: release v0.19.2

* add changelog

---------

Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>

* speed up plugin sync and several small fixes

* concepts for security improvements and process stability

* improve concept

* security concept implemented

* remove insecure TLS

* worker concept implemented

* fixes and cleanup

* fixes

* docu

* mermaid, namings

* WASM plugin host improvements, plugin logging

* fix db migration

* Improve plugin config and category mapping UI

* fixes

* further fixes

* remove manual test sync

* fix db migration and strava mapping

* type added, UI improvements

* fix plugin card toggle clickable area

* optimize synch status card layout

* plugin type 'trails' instead of 'integration'

* session auth validation in UI

* fix komoot date and waypoints

* improve category mapping

* fix send to hammerhead: trail name

* plugin setup error handling improved

* fix review findings

* re-mapping added

* rename remote_category

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
2026-06-22 15:00:44 +02:00

289 lines
8.0 KiB
Go

package pluginsystem
import (
"context"
"net/http"
"testing"
)
func TestValidateAuthContext(t *testing.T) {
tests := []struct {
name string
context AuthContext
wantErr bool
}{
{
name: "oauth2",
context: AuthContext{
Type: AuthTypeOAuth2,
AuthorizationURL: "https://example.com/oauth/authorize",
TokenURL: "https://example.com/oauth/token",
Refresh: &AuthRefresh{Mode: AuthRefreshModeHost},
},
},
{
name: "missing bearer secret",
context: AuthContext{Type: AuthTypeBearer},
wantErr: true,
},
{
name: "session",
context: AuthContext{
Type: AuthTypeSession,
SecretFields: []string{"email", "password"},
Refresh: &AuthRefresh{Mode: AuthRefreshModePlugin, Function: "refresh_session_v1"},
},
},
{
name: "unsupported",
context: AuthContext{Type: "mtls"},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := ValidateAuthContext("default", test.context)
if test.wantErr && err == nil {
t.Fatal("expected error")
}
if !test.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestInjectHostRequestAuthWithBearer(t *testing.T) {
spec := HostRequestSpec{Auth: "account"}
err := InjectHostRequestAuth(context.Background(), AuthInjectionInput{
Plugin: LocalPlugin{Manifest: Manifest{
Auth: AuthManifest{Contexts: map[string]AuthContext{
"account": {
Type: AuthTypeBearer,
SecretField: "token",
},
}},
Permissions: PermissionManifest{Auth: []string{"account"}},
}},
Auth: map[string]any{"token": "abc123"},
Spec: &spec,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := spec.Headers[AuthHeaderAuthorization]; got != AuthSchemeBearer+" abc123" {
t.Fatalf("unexpected authorization header: %q", got)
}
}
func TestInjectHostRequestAuthWithAPIKeyQuery(t *testing.T) {
spec := HostRequestSpec{
Auth: "account",
Target: RequestTarget{
Type: "connector",
Connector: "api",
Path: "/upload",
Query: []QueryParam{{Name: "existing", Value: "true"}},
},
}
err := InjectHostRequestAuth(context.Background(), AuthInjectionInput{
Plugin: LocalPlugin{Manifest: Manifest{
Auth: AuthManifest{Contexts: map[string]AuthContext{
"account": {
Type: AuthTypeAPIKey,
SecretField: "apiKey",
Placement: AuthPlacementQuery,
Name: "key",
},
}},
Permissions: PermissionManifest{Auth: []string{"account"}},
}},
Auth: map[string]any{"apiKey": "secret"},
Spec: &spec,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(spec.Target.Query) != 2 || spec.Target.Query[1].Name != "key" || spec.Target.Query[1].Value != "secret" {
t.Fatalf("unexpected query: %#v", spec.Target.Query)
}
}
func TestInjectHostRequestAuthFromPolicyWithBearer(t *testing.T) {
spec := HostRequestSpec{
Auth: "account",
Headers: map[string]string{AuthHeaderAuthorization: "plugin supplied"},
}
manifest := Manifest{
Auth: AuthManifest{Contexts: map[string]AuthContext{
"account": {Type: AuthTypeBearer, SecretField: "token"},
}},
Permissions: PermissionManifest{Auth: []string{"account"}},
}
err := InjectHostRequestAuthFromPolicy(manifest, map[string]any{"token": "host-secret"}, &spec)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := spec.Headers[AuthHeaderAuthorization]; got != AuthSchemeBearer+" host-secret" {
t.Fatalf("unexpected authorization header: %q", got)
}
}
func TestInjectHostRequestAuthFromPolicyFailsWithEmptyAuth(t *testing.T) {
spec := HostRequestSpec{
Auth: "account",
Headers: map[string]string{AuthHeaderAuthorization: "plugin supplied"},
}
manifest := Manifest{
Auth: AuthManifest{Contexts: map[string]AuthContext{
"account": {Type: AuthTypeBearer, SecretField: "token"},
}},
Permissions: PermissionManifest{Auth: []string{"account"}},
}
err := InjectHostRequestAuthFromPolicy(manifest, map[string]any{}, &spec)
if err == nil {
t.Fatal("expected error")
}
if err.Error() != "bearer token is missing" {
t.Fatalf("unexpected error: %v", err)
}
if got := spec.Headers[AuthHeaderAuthorization]; got != "plugin supplied" {
t.Fatalf("unexpected authorization header mutation: %q", got)
}
}
func TestInjectHostRequestAuthFromPolicyRejectsSessionAuth(t *testing.T) {
spec := HostRequestSpec{Auth: "account"}
manifest := Manifest{
Auth: AuthManifest{Contexts: map[string]AuthContext{
"account": {
Type: AuthTypeSession,
SecretFields: []string{"password"},
Refresh: &AuthRefresh{Mode: AuthRefreshModePlugin, Function: "refresh_session_v1"},
},
}},
Permissions: PermissionManifest{Auth: []string{"account"}},
}
err := InjectHostRequestAuthFromPolicy(manifest, map[string]any{"password": "secret"}, &spec)
if err == nil {
t.Fatal("expected error")
}
if err.Error() != "session auth requires handler-managed injection" {
t.Fatalf("unexpected error: %v", err)
}
}
func TestInjectHostRequestAuthFromPolicyWithAPIKeyQuery(t *testing.T) {
spec := HostRequestSpec{
Auth: "account",
Target: RequestTarget{
Type: "connector",
Path: "/assets",
Query: []QueryParam{{Name: "api_key", Value: "plugin"}},
},
}
manifest := Manifest{
Auth: AuthManifest{Contexts: map[string]AuthContext{
"account": {
Type: AuthTypeAPIKey,
SecretField: "apiKey",
Placement: AuthPlacementQuery,
Name: "api_key",
},
}},
Permissions: PermissionManifest{Auth: []string{"account"}},
}
err := InjectHostRequestAuthFromPolicy(manifest, map[string]any{"apiKey": "host-secret"}, &spec)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(spec.Target.Query) != 1 || spec.Target.Query[0].Value != "host-secret" {
t.Fatalf("unexpected query: %#v", spec.Target.Query)
}
}
func TestInjectHostRequestAuthRequiresSpec(t *testing.T) {
err := InjectHostRequestAuth(context.Background(), AuthInjectionInput{})
if err == nil {
t.Fatal("expected error")
}
if err.Error() != "host request spec is required" {
t.Fatalf("unexpected error: %v", err)
}
}
func TestInjectHostRequestAuthValidatesPermission(t *testing.T) {
spec := HostRequestSpec{Auth: "account"}
err := InjectHostRequestAuth(context.Background(), AuthInjectionInput{
Plugin: LocalPlugin{Manifest: Manifest{
Auth: AuthManifest{Contexts: map[string]AuthContext{
"account": {Type: AuthTypeBearer, SecretField: "token"},
}},
}},
Auth: map[string]any{"token": "abc123"},
Spec: &spec,
})
if err == nil {
t.Fatal("expected error")
}
if err.Error() != `auth context "account" is not permitted` {
t.Fatalf("unexpected error: %v", err)
}
}
func TestInjectRequestAuthForContextPreservesQueryOrder(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "https://example.test/media?z=last&api_key=plugin&a=first", nil)
if err != nil {
t.Fatal(err)
}
manifest := Manifest{
Auth: AuthManifest{Contexts: map[string]AuthContext{
"account": {
Type: AuthTypeAPIKey,
SecretField: "apiKey",
Placement: AuthPlacementQuery,
Name: "api_key",
},
}},
Permissions: PermissionManifest{Auth: []string{"account"}},
}
err = InjectRequestAuthForContext(manifest, map[string]any{"apiKey": "host-secret"}, "account", req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if req.URL.RawQuery != "z=last&a=first&api_key=host-secret" {
t.Fatalf("unexpected raw query: %q", req.URL.RawQuery)
}
}
func TestAuthForPluginRefresh(t *testing.T) {
filtered := AuthForPluginRefresh(map[string]any{
"email": "user@example.com",
"password": "secret",
"accessToken": "token",
}, AuthContext{
Fields: []string{"email", "password"},
SecretFields: []string{"password"},
})
if len(filtered) != 2 {
t.Fatalf("unexpected filtered auth: %#v", filtered)
}
if filtered["email"] != "user@example.com" || filtered["password"] != "secret" {
t.Fatalf("unexpected filtered auth: %#v", filtered)
}
if _, ok := filtered["accessToken"]; ok {
t.Fatalf("unexpected access token in plugin refresh auth: %#v", filtered)
}
}