mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
hanzoai/authz v1.10.42 decides Sudo on the person's own org, the first entry of orgs, so a brand org's user holding an admin-org membership no longer mirrors onto _superusers, and that membership no longer selects another tenant's Base or the admin org's. The fixtures that stood for a SuperAdmin now carry admin first, and TestIAMSuperuserMirror, TestOperatorMayReachATenant and TestOperatorReachesTheOrgItNames each refuse the brand member beside the operator they admit. Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
261 lines
10 KiB
Go
261 lines
10 KiB
Go
package apis_test
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/hanzoai/base/apis"
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tests"
|
|
)
|
|
|
|
func TestRecordAuthRefresh(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
iamToken, jwksURL := mintIAMToken(t, jwt.MapClaims{
|
|
"sub": "admin/z", "owner": "hanzo", "email": "[email protected]",
|
|
"orgs": []any{
|
|
map[string]any{"org": "admin", "role": "owner"},
|
|
map[string]any{"org": "hanzo", "role": "admin"},
|
|
},
|
|
})
|
|
|
|
scenarios := []tests.ApiScenario{
|
|
{
|
|
Name: "unauthorized",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/users/auth-refresh",
|
|
ExpectedStatus: 401,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "superuser trying to refresh the auth of another auth collection",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/users/auth-refresh",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "superuser token a Base signed",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/_superusers/auth-refresh",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "superuser token from IAM",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/_superusers/auth-refresh",
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer " + iamToken,
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
app.Store().Set(apis.StoreKeyExternalAuthOnly, true)
|
|
app.Store().Set(apis.StoreKeyJWKSURL, jwksURL)
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"token":"` + iamToken + `"`,
|
|
`"collectionName":"_superusers"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnRecordAuthRefreshRequest": 1,
|
|
"OnRecordAuthRequest": 1,
|
|
"OnRecordEnrich": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "auth record + not an auth collection",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/demo1/auth-refresh",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "auth record + different auth collection",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/clients/auth-refresh?expand=rel,missing",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "auth record + same auth collection as the token",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/users/auth-refresh?expand=rel,missing",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"token":`,
|
|
`"record":`,
|
|
`"id":"4q1xlclmfloku33"`,
|
|
`"emailVisibility":false`,
|
|
`"email":"[email protected]"`, // the owner can always view their email address
|
|
`"expand":`,
|
|
`"rel":`,
|
|
`"id":"llvuca81nly1qls"`,
|
|
},
|
|
NotExpectedContent: []string{
|
|
`"missing":`,
|
|
// should return a different token
|
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnRecordAuthRefreshRequest": 1,
|
|
"OnRecordAuthRequest": 1,
|
|
"OnRecordEnrich": 2,
|
|
},
|
|
},
|
|
{
|
|
Name: "auth record + same auth collection as the token but static/unrefreshable",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/users/auth-refresh",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOmZhbHNlLCJ0eXBlIjoiYXV0aCJ9.8LJt8Jz66YTdn5bm_PalFa66lhgdZINXn5EDiVZNV7M",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
// should return the same token
|
|
`"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOmZhbHNlLCJ0eXBlIjoiYXV0aCJ9.8LJt8Jz66YTdn5bm_PalFa66lhgdZINXn5EDiVZNV7M"`,
|
|
`"record":`,
|
|
`"id":"4q1xlclmfloku33"`,
|
|
`"emailVisibility":false`,
|
|
`"email":"[email protected]"`, // the owner can always view their email address
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnRecordAuthRefreshRequest": 1,
|
|
"OnRecordAuthRequest": 1,
|
|
"OnRecordEnrich": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "unverified auth record in onlyVerified collection",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/clients/auth-refresh",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoibzF5MGRkMHNwZDc4Nm1kIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.Vk_K1eyZL_I1VD6fWPHfkA_lBmtbw-fwPq3FSfsyoY8",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnRecordAuthRefreshRequest": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "verified auth record in onlyVerified collection",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/clients/auth-refresh",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZ2szOTBxZWdzNHk0N3duIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.xCGkWuACPNAEUBLQVK4KKp72HzA2aOtWZnP47iBs5os",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"token":`,
|
|
`"record":`,
|
|
`"id":"gk390qegs4y47wn"`,
|
|
`"verified":true`,
|
|
`"email":"[email protected]"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnRecordAuthRefreshRequest": 1,
|
|
"OnRecordAuthRequest": 1,
|
|
"OnRecordEnrich": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "OnRecordAuthRefreshRequest tx body write check",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/users/auth-refresh",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
app.OnRecordAuthRefreshRequest().BindFunc(func(e *core.RecordAuthRefreshRequestEvent) error {
|
|
original := e.App
|
|
return e.App.RunInTransaction(func(txApp core.App) error {
|
|
e.App = txApp
|
|
defer func() { e.App = original }()
|
|
|
|
if err := e.Next(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return e.BadRequestError("TX_ERROR", nil)
|
|
})
|
|
})
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedEvents: map[string]int{"OnRecordAuthRefreshRequest": 1},
|
|
ExpectedContent: []string{"TX_ERROR"},
|
|
},
|
|
|
|
// rate limit checks
|
|
// -----------------------------------------------------------
|
|
{
|
|
Name: "RateLimit rule - users:authRefresh",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/users/auth-refresh",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
app.Settings().RateLimits.Enabled = true
|
|
app.Settings().RateLimits.Rules = []core.RateLimitRule{
|
|
{MaxRequests: 100, Label: "abc"},
|
|
{MaxRequests: 100, Label: "*:authRefresh"},
|
|
{MaxRequests: 0, Label: "users:authRefresh"},
|
|
}
|
|
},
|
|
ExpectedStatus: 429,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "RateLimit rule - *:authRefresh",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/users/auth-refresh",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
app.Settings().RateLimits.Enabled = true
|
|
app.Settings().RateLimits.Rules = []core.RateLimitRule{
|
|
{MaxRequests: 100, Label: "abc"},
|
|
{MaxRequests: 0, Label: "*:authRefresh"},
|
|
}
|
|
},
|
|
ExpectedStatus: 429,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
scenario.Test(t)
|
|
}
|
|
}
|