|
4 | 4 | "bytes" |
5 | 5 | "context" |
6 | 6 | "encoding/json" |
| 7 | + "errors" |
7 | 8 | "net/http" |
8 | 9 | "net/http/httptest" |
9 | 10 | "strings" |
@@ -294,6 +295,79 @@ func TestCreateSpritzRejectsExternalOwnerResolutionWithoutCreateScopes(t *testin |
294 | 295 | } |
295 | 296 | } |
296 | 297 |
|
| 298 | +func TestCreateSpritzRejectsExternalOwnerForAdminCallers(t *testing.T) { |
| 299 | + s := newCreateSpritzTestServer(t) |
| 300 | + configureProvisionerTestServer(s) |
| 301 | + resolverCalls := 0 |
| 302 | + configureExternalOwnerTestServer(s, fakeExternalOwnerResolver{ |
| 303 | + resolve: func(_ context.Context, _ externalOwnerPolicy, _ principal, _ ownerRef, _ string) (externalOwnerResolution, error) { |
| 304 | + resolverCalls++ |
| 305 | + return externalOwnerResolution{ |
| 306 | + Status: externalOwnerResolved, |
| 307 | + OwnerID: "user-123", |
| 308 | + }, nil |
| 309 | + }, |
| 310 | + }) |
| 311 | + e := newCreateSpritzAPI(t, s) |
| 312 | + |
| 313 | + body := []byte(`{"presetId":"openclaw","ownerRef":{"type":"external","provider":"msteams","tenant":"72f988bf-86f1-41af-91ab-2d7cd011db47","subject":"6f0f9d4f-9b0e-4d52-8c3a-ef0fd64b9b9f"},"idempotencyKey":"teams-admin"}`) |
| 314 | + req := httptest.NewRequest(http.MethodPost, "/api/spritzes", bytes.NewReader(body)) |
| 315 | + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) |
| 316 | + req.Header.Set("X-Spritz-User-Id", "admin-1") |
| 317 | + req.Header.Set("X-Spritz-Principal-Type", "admin") |
| 318 | + rec := httptest.NewRecorder() |
| 319 | + |
| 320 | + e.ServeHTTP(rec, req) |
| 321 | + |
| 322 | + if rec.Code != http.StatusForbidden { |
| 323 | + t.Fatalf("expected status 403, got %d: %s", rec.Code, rec.Body.String()) |
| 324 | + } |
| 325 | + if resolverCalls != 0 { |
| 326 | + t.Fatalf("expected resolver to be skipped for admin callers, got %d calls", resolverCalls) |
| 327 | + } |
| 328 | +} |
| 329 | + |
| 330 | +func TestExternalOwnerResolveRequiresTenantWhenTenantAllowlistIsConfigured(t *testing.T) { |
| 331 | + config := externalOwnerConfig{ |
| 332 | + subjectHashKey: []byte("test-external-owner-secret"), |
| 333 | + policies: map[string]externalOwnerPolicy{ |
| 334 | + "zenobot": { |
| 335 | + PrincipalID: "zenobot", |
| 336 | + Issuer: "zenobot", |
| 337 | + URL: "http://resolver.example.com/v1/external-owners/resolve", |
| 338 | + AllowedProviders: map[string]struct{}{ |
| 339 | + "slack": {}, |
| 340 | + }, |
| 341 | + AllowedTenants: map[string]struct{}{ |
| 342 | + "enterprise-1": {}, |
| 343 | + }, |
| 344 | + }, |
| 345 | + }, |
| 346 | + resolver: fakeExternalOwnerResolver{ |
| 347 | + resolve: func(_ context.Context, _ externalOwnerPolicy, _ principal, _ ownerRef, _ string) (externalOwnerResolution, error) { |
| 348 | + t.Fatal("expected resolver call to be blocked when tenant is missing") |
| 349 | + return externalOwnerResolution{}, nil |
| 350 | + }, |
| 351 | + }, |
| 352 | + } |
| 353 | + |
| 354 | + _, err := config.resolve(context.Background(), principal{ID: "zenobot", Type: principalTypeService}, ownerRef{ |
| 355 | + Type: "external", |
| 356 | + Provider: "slack", |
| 357 | + Subject: "U123456", |
| 358 | + }, "") |
| 359 | + if err == nil { |
| 360 | + t.Fatal("expected resolve to fail when tenant allowlist is configured but tenant is missing") |
| 361 | + } |
| 362 | + var resolutionErr externalOwnerResolutionError |
| 363 | + if !errors.As(err, &resolutionErr) { |
| 364 | + t.Fatalf("expected externalOwnerResolutionError, got %T", err) |
| 365 | + } |
| 366 | + if resolutionErr.code != "external_identity_forbidden" { |
| 367 | + t.Fatalf("expected external_identity_forbidden, got %q", resolutionErr.code) |
| 368 | + } |
| 369 | +} |
| 370 | + |
297 | 371 | func TestCreateRequestFingerprintCanonicalizesEquivalentOwnerInputs(t *testing.T) { |
298 | 372 | directFingerprint, err := createRequestFingerprint(createRequest{ |
299 | 373 | OwnerID: "user-123", |
@@ -372,3 +446,16 @@ func TestNormalizeCreateOwnerSupportsOwnerRefOwner(t *testing.T) { |
372 | 446 | t.Fatalf("expected body ownerId to be populated from ownerRef, got %q", body.OwnerID) |
373 | 447 | } |
374 | 448 | } |
| 449 | + |
| 450 | +func TestNormalizeCreateOwnerRejectsOwnerRefWithoutType(t *testing.T) { |
| 451 | + body := &createRequest{ |
| 452 | + OwnerRef: &ownerRef{ID: "user-123"}, |
| 453 | + } |
| 454 | + _, err := normalizeCreateOwnerRequest(body, principal{ID: "user-123", Type: principalTypeHuman}, true) |
| 455 | + if err == nil { |
| 456 | + t.Fatal("expected normalizeCreateOwnerRequest to reject ownerRef without type") |
| 457 | + } |
| 458 | + if !strings.Contains(err.Error(), "ownerRef.type is required") { |
| 459 | + t.Fatalf("expected ownerRef.type validation error, got %v", err) |
| 460 | + } |
| 461 | +} |
0 commit comments