Files
opsx-vpn/backend/internal/identityprovider/admission_test.go
T

140 lines
6.1 KiB
Go

package identityprovider
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"vpn-platform/backend/internal/model"
"vpn-platform/backend/internal/store"
)
func TestDirectoryOnlyAcceptsMatchingOpenIDAndName(t *testing.T) {
previous := upstreamClient
t.Cleanup(func() { upstreamClient = previous })
response := `{"code":0,"data":{"users":[{"user_id":"ou_123","name":"Li Ge"}]}}`
fallback := `{"code":99991672}`
upstreamClient = &http.Client{Transport: transportFunc(func(req *http.Request) (*http.Response, error) {
body := `{"code":0,"tenant_access_token":"temporary"}`
switch req.URL.String() {
case feishuTenantToken:
case feishuBasicBatch:
if req.Method != http.MethodPost || req.Header.Get("Authorization") != "Bearer temporary" || req.Header.Get("Content-Type") != "application/json" {
t.Fatal("invalid basic batch request")
}
var payload struct {
UserIDs []string `json:"user_ids"`
}
if json.NewDecoder(req.Body).Decode(&payload) != nil || len(payload.UserIDs) != 1 || payload.UserIDs[0] != "ou_123" {
t.Fatal("wrong open_id in basic batch")
}
body = response
case feishuContactUser + "ou_123?user_id_type=open_id":
body = fallback
default:
t.Fatalf("unexpected directory request: %s", req.URL)
}
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
})}
if name, source, err := fetchDirectoryNameWithSource(context.Background(), "cli_app", "secret", "ou_123"); err != nil || name != "Li Ge" || source != "basic_batch" {
t.Fatalf("directory: %q %q %v", name, source, err)
}
for _, reply := range []string{`{"code":0,"data":{"users":[{"user_id":"ou_other","name":"Wrong"}]}}`, `{"code":0,"data":{"users":[{"name":"No ID"}]}}`, `{"code":99991672,"msg":"secret"}`} {
response = reply
if _, err := fetchDirectoryName(context.Background(), "cli_app", "secret", "ou_123"); err == nil {
t.Fatal("accepted unverified directory identity")
}
}
response = `{"code":99991672}`
fallback = `{"code":0,"data":{"user":{"open_id":"ou_123","name":"Directory name"}}}`
if name, source, err := fetchDirectoryNameWithSource(context.Background(), "cli_app", "secret", "ou_123"); err != nil || name != "Directory name" || source != "contact_user" {
t.Fatalf("fallback: %q %q %v", name, source, err)
}
upstreamClient = &http.Client{Transport: transportFunc(func(req *http.Request) (*http.Response, error) {
if req.URL.String() == feishuTenantToken {
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(`{"code":0,"tenant_access_token":"temporary"}`)), Header: make(http.Header)}, nil
}
return &http.Response{StatusCode: 403, Body: io.NopCloser(strings.NewReader(`{"code":99991672,"msg":"private upstream detail"}`)), Header: make(http.Header)}, nil
})}
if _, err := fetchDirectoryName(context.Background(), "cli_app", "secret", "ou_123"); err == nil || strings.Contains(err.Error(), "private upstream detail") {
t.Fatal("directory permission failure leaked upstream details")
}
upstreamClient = &http.Client{Transport: transportFunc(func(req *http.Request) (*http.Response, error) { return nil, req.Context().Err() })}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := fetchDirectoryName(ctx, "cli_app", "secret", "ou_123"); err == nil {
t.Fatal("cancelled directory request succeeded")
}
}
func TestAdmissionDefaultsToAllAndScopesGrants(t *testing.T) {
previous := store.DB
t.Cleanup(func() {
if store.DB != nil {
if db, err := store.DB.DB(); err == nil {
_ = db.Close()
}
}
store.DB = previous
})
if err := store.Init(t.TempDir()); err != nil {
t.Fatal(err)
}
base := "https://vpn.example.com/id"
redirect := "https://vpn.example.com/oauth2/callback"
idp := model.IdentityProvider{AppID: "cli_current", ClientID: "vpn_client", TenantKey: "tenant"}
tunnel := model.Tunnel{Name: "feishu", Protocol: "openvpn", Config: `{"auth_mode":"oidc","oidc_provider":"builtin","oidc_issuer":"https://vpn.example.com/id","oidc_client_id":"vpn_client","oidc_base_url":"https://vpn.example.com"}`}
if err := store.DB.Create(&tunnel).Error; err != nil {
t.Fatal(err)
}
if ok, err := admitted(redirect, base, idp, "tenant:ou_1"); err != nil || !ok {
t.Fatalf("default: %v %v", ok, err)
}
if err := store.DB.Model(&tunnel).Update("config", `{"auth_mode":"oidc","oidc_provider":"builtin","oidc_issuer":"https://vpn.example.com/id","oidc_client_id":"vpn_client","oidc_base_url":"https://vpn.example.com","feishu_access_mode":true}`).Error; err != nil {
t.Fatal(err)
}
if ok, err := admitted(redirect, base, idp, "tenant:ou_1"); err == nil || ok {
t.Fatal("invalid policy admitted")
}
if err := store.DB.Model(&tunnel).Update("config", tunnel.Config).Error; err != nil {
t.Fatal(err)
}
if ok, _ := admitted("https://wrong.example.com/oauth2/callback", base, idp, "tenant:ou_1"); ok {
t.Fatal("unknown callback admitted")
}
tunnel.Config = `{"auth_mode":"oidc","oidc_provider":"builtin","oidc_issuer":"https://vpn.example.com/id","oidc_client_id":"vpn_client","oidc_base_url":"https://vpn.example.com","feishu_access_mode":"allowlist"}`
if err := store.DB.Save(&tunnel).Error; err != nil {
t.Fatal(err)
}
if ok, err := admitted(redirect, base, idp, "tenant:ou_1"); err != nil || ok {
t.Fatalf("unauthorized: %v %v", ok, err)
}
grant := model.FeishuTunnelGrant{TunnelID: tunnel.ID, Issuer: base, AppID: idp.AppID, Subject: "tenant:ou_1"}
if err := store.DB.Create(&grant).Error; err != nil {
t.Fatal(err)
}
if ok, err := admitted(redirect, base, idp, grant.Subject); err != nil || !ok {
t.Fatalf("grant: %v %v", ok, err)
}
idp.AppID = "cli_other"
if ok, _ := admitted(redirect, base, idp, grant.Subject); ok {
t.Fatal("cross-app grant admitted")
}
idp.AppID = "cli_current"
if err := store.DB.Delete(&grant).Error; err != nil {
t.Fatal(err)
}
if ok, _ := admitted(redirect, base, idp, grant.Subject); ok {
t.Fatal("revoked grant admitted")
}
if err := store.DB.Migrator().DropTable(&model.FeishuTunnelGrant{}); err != nil {
t.Fatal(err)
}
if ok, err := admitted(redirect, base, idp, grant.Subject); ok || err == nil {
t.Fatal("database failure admitted")
}
}