125 lines
6.1 KiB
Go
125 lines
6.1 KiB
Go
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.pcux.cn/xuans/Smail/hooks"
|
|
"git.pcux.cn/xuans/Smail/models"
|
|
"git.pcux.cn/xuans/Smail/services/access"
|
|
appcontext "git.pcux.cn/xuans/Smail/utils/context"
|
|
)
|
|
|
|
func callPluginSecret(ctx *appcontext.Context, method, pluginID, body string) *httptest.ResponseRecorder {
|
|
recorder := httptest.NewRecorder()
|
|
PluginSecret(ctx, recorder, httptest.NewRequest(method, "/api/v1/plugin/secrets/"+pluginID, strings.NewReader(body)))
|
|
return recorder
|
|
}
|
|
|
|
func TestPluginSecretAuthorization(t *testing.T) {
|
|
service, engine := newTelegramControllerConfigService(t)
|
|
previous := hooks.PluginConfigService()
|
|
hooks.SetPluginConfigService(service)
|
|
t.Cleanup(func() { hooks.SetPluginConfigService(previous) })
|
|
for _, tc := range []struct {
|
|
name string
|
|
ctx *appcontext.Context
|
|
code string
|
|
}{
|
|
{"anonymous", nil, "ACCESS_DENIED"},
|
|
{"no session user", &appcontext.Context{Role: access.RoleSuperAdmin, RecentAuth: true, RecoveryCodeConfigured: true}, "ACCESS_DENIED"},
|
|
{"ordinary user", &appcontext.Context{UserID: 3, Role: access.RoleUser, RecentAuth: true, RecoveryCodeConfigured: true}, "ACCESS_DENIED"},
|
|
{"missing recent authentication", &appcontext.Context{UserID: 2, Role: access.RoleAdmin, RecoveryCodeConfigured: true}, "RECENT_AUTH_REQUIRED"},
|
|
{"missing recovery code", &appcontext.Context{UserID: 2, Role: access.RoleAdmin, RecentAuth: true}, "RECOVERY_CODE_REQUIRED"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
recorder := callPluginSecret(tc.ctx, http.MethodPut, "telegram_push", `{"version":0,"change":{"operation":"replace","value":{"plaintext":"test-only-secret"}}}`)
|
|
if recorder.Code != http.StatusForbidden || !strings.Contains(recorder.Body.String(), tc.code) {
|
|
t.Fatalf("status=%d response=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
count, err := engine.Count(new(models.PluginSecret))
|
|
if err != nil || count != 0 {
|
|
t.Fatalf("denied writes changed secrets: count=%d err=%v", count, err)
|
|
}
|
|
for _, role := range []access.Role{access.RoleAdmin, access.RoleSuperAdmin} {
|
|
recorder := callPluginSecret(&appcontext.Context{UserID: 2, Role: role}, http.MethodGet, "telegram_push", "")
|
|
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"configured":false`) {
|
|
t.Fatalf("read role=%s status=%d body=%s", role, recorder.Code, recorder.Body.String())
|
|
}
|
|
}
|
|
recorder := callPluginSecret(&appcontext.Context{UserID: 3, Role: access.RoleUser}, http.MethodGet, "telegram_push", "")
|
|
if recorder.Code != http.StatusForbidden {
|
|
t.Fatalf("ordinary user read status=%d", recorder.Code)
|
|
}
|
|
}
|
|
|
|
func TestPluginSecretEncryptedLifecycleAndValidation(t *testing.T) {
|
|
service, engine := newTelegramControllerConfigService(t)
|
|
previous := hooks.PluginConfigService()
|
|
hooks.SetPluginConfigService(service)
|
|
t.Cleanup(func() { hooks.SetPluginConfigService(previous) })
|
|
ctx := &appcontext.Context{UserID: 2, Role: access.RoleAdmin, RecentAuth: true, RecoveryCodeConfigured: true}
|
|
for _, id := range []string{"telegram_push", "wechat_push"} {
|
|
t.Run(id, func(t *testing.T) {
|
|
for _, body := range []string{
|
|
`{"version":0,"change":{"operation":"replace","value":{"plaintext":"test-only-secret"}},"userId":99}`,
|
|
`{"version":0,"change":{"operation":"replace","value":{"plaintext":"test-only-secret","extra":true}}}`,
|
|
`{"version":0,"change":{"operation":"replace","value":{"plaintext":"test-only-secret"}}} {}`,
|
|
`{"version":0,"change":{"operation":"replace","value":{"plaintext":""}}}`,
|
|
`{"version":0,"change":{"operation":"replace","value":{"plaintext":"` + strings.Repeat("x", 17000) + `"}}}`,
|
|
} {
|
|
recorder := callPluginSecret(ctx, http.MethodPut, id, body)
|
|
if strings.Contains(recorder.Body.String(), `"errorNo":0`) {
|
|
t.Fatal("invalid request accepted")
|
|
}
|
|
}
|
|
body := `{"version":0,"change":{"operation":"replace","value":{"plaintext":"test-only-secret"}}}`
|
|
recorder := callPluginSecret(ctx, http.MethodPut, id, body)
|
|
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"version":1`) {
|
|
t.Fatalf("save status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
if recorder.Header().Get("Cache-Control") != "no-store" || strings.Contains(recorder.Body.String(), "test-only-secret") {
|
|
t.Fatal("unsafe secret response")
|
|
}
|
|
record := models.PluginSecret{}
|
|
found, err := engine.Where("plugin_id = ?", id).Get(&record)
|
|
if err != nil || !found || record.Ciphertext == "" || record.Ciphertext == "test-only-secret" || record.KeyVersion == 0 {
|
|
t.Fatalf("secret was not encrypted: found=%v err=%v", found, err)
|
|
}
|
|
recorder = callPluginSecret(ctx, http.MethodGet, id, "")
|
|
var payload struct {
|
|
Data map[string]any `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &payload); err != nil || len(payload.Data) != 4 || payload.Data["configured"] != true || payload.Data["version"] != float64(1) {
|
|
t.Fatalf("unexpected public status: %s", recorder.Body.String())
|
|
}
|
|
if strings.Contains(recorder.Body.String(), record.Ciphertext) || strings.Contains(recorder.Body.String(), "test-only-secret") {
|
|
t.Fatal("secret leaked in status")
|
|
}
|
|
recorder = callPluginSecret(ctx, http.MethodPut, id, body)
|
|
if recorder.Code != http.StatusConflict {
|
|
t.Fatalf("stale version accepted: %s", recorder.Body.String())
|
|
}
|
|
recorder = callPluginSecret(ctx, http.MethodPut, id, `{"version":1,"change":{"operation":"clear"}}`)
|
|
if strings.Contains(recorder.Body.String(), `"errorNo":0`) {
|
|
t.Fatal("unconfirmed clear accepted")
|
|
}
|
|
recorder = callPluginSecret(ctx, http.MethodPut, id, `{"version":1,"change":{"operation":"clear","confirmed":true}}`)
|
|
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"configured":false`) {
|
|
t.Fatalf("clear failed: %s", recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
if recorder := callPluginSecret(ctx, http.MethodDelete, "telegram_push", ""); recorder.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("unsupported method status=%d", recorder.Code)
|
|
}
|
|
if recorder := callPluginSecret(ctx, http.MethodPut, "unknown", `{}`); recorder.Code != http.StatusNotFound {
|
|
t.Fatalf("unknown plugin status=%d", recorder.Code)
|
|
}
|
|
}
|