zitadel/internal/api/grpc/system/limits_integration_block_test.go
Livio Spring b6c10c4c83
fix: consider oidc session events for authN milestones (#8089)
# Which Problems Are Solved

After migrating the access token events in #7822, milestones based on
authentication, resp. theses events would not be reached.

# How the Problems Are Solved

Additionally use the `oidc_session.Added` event to check for
`milestone.AuthenticationSucceededOnInstance` and
`milestone.AuthenticationSucceededOnApplication`.

# Additional Changes

None.

# Additional Context

- relates to #7822 
- noticed internally
2024-06-12 06:49:14 +02:00

288 lines
9.5 KiB
Go

//go:build integration
package system_test
import (
"fmt"
"io"
"net"
"net/http"
"strings"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/system"
)
func TestServer_Limits_Block(t *testing.T) {
domain, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
tests := []*test{
publicAPIBlockingTest(domain),
{
name: "mutating API",
testGrpc: func(tt assert.TestingT, expectBlocked bool) {
randomGrpcIdpName := randomString("idp-grpc", 5)
_, err := Tester.Client.Admin.AddGitHubProvider(iamOwnerCtx, &admin.AddGitHubProviderRequest{
Name: randomGrpcIdpName,
ClientId: "client-id",
ClientSecret: "client-secret",
})
assertGrpcError(tt, err, expectBlocked)
//nolint:contextcheck
idpExists := idpExistsCondition(tt, instanceID, randomGrpcIdpName)
if expectBlocked {
// We ensure that the idp really is not created
assert.Neverf(tt, idpExists, 5*time.Second, 1*time.Second, "idp should never be created")
} else {
assert.Eventuallyf(tt, idpExists, 5*time.Second, 1*time.Second, "idp should be created")
}
},
testHttp: func(tt assert.TestingT) (*http.Request, error, func(assert.TestingT, *http.Response, bool)) {
randomHttpIdpName := randomString("idp-http", 5)
req, err := http.NewRequestWithContext(
CTX,
"POST",
fmt.Sprintf("http://%s/admin/v1/idps/github", net.JoinHostPort(domain, "8080")),
strings.NewReader(`{
"name": "`+randomHttpIdpName+`",
"clientId": "client-id",
"clientSecret": "client-secret"
}`),
)
if err != nil {
return nil, err, nil
}
req.Header.Set("Authorization", Tester.BearerToken(iamOwnerCtx))
return req, nil, func(ttt assert.TestingT, response *http.Response, expectBlocked bool) {
assertLimitResponse(ttt, response, expectBlocked)
assertSetLimitingCookie(ttt, response, expectBlocked)
}
},
}, {
name: "discovery",
testHttp: func(tt assert.TestingT) (*http.Request, error, func(assert.TestingT, *http.Response, bool)) {
req, err := http.NewRequestWithContext(
CTX,
"GET",
fmt.Sprintf("http://%s/.well-known/openid-configuration", net.JoinHostPort(domain, "8080")),
nil,
)
return req, err, func(ttt assert.TestingT, response *http.Response, expectBlocked bool) {
assertLimitResponse(ttt, response, expectBlocked)
assertSetLimitingCookie(ttt, response, expectBlocked)
}
},
}, {
name: "login",
testHttp: func(tt assert.TestingT) (*http.Request, error, func(assert.TestingT, *http.Response, bool)) {
req, err := http.NewRequestWithContext(
CTX,
"GET",
fmt.Sprintf("http://%s/ui/login/login/externalidp/callback", net.JoinHostPort(domain, "8080")),
nil,
)
return req, err, func(ttt assert.TestingT, response *http.Response, expectBlocked bool) {
// the login paths should return a redirect if the instance is blocked
if expectBlocked {
assert.Equal(ttt, http.StatusFound, response.StatusCode)
} else {
assertLimitResponse(ttt, response, false)
}
assertSetLimitingCookie(ttt, response, expectBlocked)
}
},
}, {
name: "console",
testHttp: func(tt assert.TestingT) (*http.Request, error, func(assert.TestingT, *http.Response, bool)) {
req, err := http.NewRequestWithContext(
CTX,
"GET",
fmt.Sprintf("http://%s/ui/console/", net.JoinHostPort(domain, "8080")),
nil,
)
return req, err, func(ttt assert.TestingT, response *http.Response, expectBlocked bool) {
// the console is not blocked so we can render a link to an instance management portal.
// A CDN can cache these assets easily
// We also don't care about a cookie because the environment.json already takes care of that.
assertLimitResponse(ttt, response, false)
}
},
}, {
name: "environment.json",
testHttp: func(tt assert.TestingT) (*http.Request, error, func(assert.TestingT, *http.Response, bool)) {
req, err := http.NewRequestWithContext(
CTX,
"GET",
fmt.Sprintf("http://%s/ui/console/assets/environment.json", net.JoinHostPort(domain, "8080")),
nil,
)
return req, err, func(ttt assert.TestingT, response *http.Response, expectBlocked bool) {
// the environment.json should always return successfully
assertLimitResponse(ttt, response, false)
assertSetLimitingCookie(ttt, response, expectBlocked)
body, err := io.ReadAll(response.Body)
assert.NoError(ttt, err)
var compFunc assert.ComparisonAssertionFunc = assert.NotContains
if expectBlocked {
compFunc = assert.Contains
}
compFunc(ttt, string(body), `"exhausted":true`)
}
},
}}
_, err := Tester.Client.System.SetLimits(SystemCTX, &system.SetLimitsRequest{
InstanceId: instanceID,
Block: gu.Ptr(true),
})
require.NoError(t, err)
// The following call ensures that an undefined bool is not deserialized to false
_, err = Tester.Client.System.SetLimits(SystemCTX, &system.SetLimitsRequest{
InstanceId: instanceID,
AuditLogRetention: durationpb.New(time.Hour),
})
require.NoError(t, err)
for _, tt := range tests {
var isFirst bool
t.Run(tt.name+" with blocking", func(t *testing.T) {
isFirst = isFirst || !t.Skipped()
testBlockingAPI(t, tt, true, isFirst)
})
}
_, err = Tester.Client.System.SetLimits(SystemCTX, &system.SetLimitsRequest{
InstanceId: instanceID,
Block: gu.Ptr(false),
})
require.NoError(t, err)
for _, tt := range tests {
var isFirst bool
t.Run(tt.name+" without blocking", func(t *testing.T) {
isFirst = isFirst || !t.Skipped()
testBlockingAPI(t, tt, false, isFirst)
})
}
}
type test struct {
name string
testHttp func(t assert.TestingT) (req *http.Request, err error, assertResponse func(t assert.TestingT, response *http.Response, expectBlocked bool))
testGrpc func(t assert.TestingT, expectBlocked bool)
}
func testBlockingAPI(t *testing.T, tt *test, expectBlocked bool, isFirst bool) {
req, err, assertResponse := tt.testHttp(t)
require.NoError(t, err)
testHTTP := func(tt assert.TestingT) {
resp, err := (&http.Client{
// Don't follow redirects
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}).Do(req)
defer func() {
require.NoError(t, resp.Body.Close())
}()
require.NoError(t, err)
assertResponse(t, resp, expectBlocked)
}
if isFirst {
// limits are eventually consistent, so we need to wait for the blocking to be set on the first test
assert.EventuallyWithT(t, func(c *assert.CollectT) {
testHTTP(c)
}, 15*time.Second, time.Second, "wait for blocking to be set")
} else {
testHTTP(t)
}
if tt.testGrpc != nil {
tt.testGrpc(t, expectBlocked)
}
}
func publicAPIBlockingTest(domain string) *test {
return &test{
name: "public API",
testGrpc: func(tt assert.TestingT, expectBlocked bool) {
conn, err := grpc.DialContext(CTX, net.JoinHostPort(domain, "8080"),
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
assert.NoError(tt, err)
_, err = admin.NewAdminServiceClient(conn).Healthz(CTX, &admin.HealthzRequest{})
assertGrpcError(tt, err, expectBlocked)
},
testHttp: func(tt assert.TestingT) (*http.Request, error, func(assert.TestingT, *http.Response, bool)) {
req, err := http.NewRequestWithContext(
CTX,
"GET",
fmt.Sprintf("http://%s/admin/v1/healthz", net.JoinHostPort(domain, "8080")),
nil,
)
return req, err, func(ttt assert.TestingT, response *http.Response, expectBlocked bool) {
assertLimitResponse(ttt, response, expectBlocked)
assertSetLimitingCookie(ttt, response, expectBlocked)
}
},
}
}
// If expectSet is true, we expect the cookie to be set
// If expectSet is false, we expect the cookie to be deleted
func assertSetLimitingCookie(t assert.TestingT, response *http.Response, expectSet bool) {
for _, cookie := range response.Cookies() {
if cookie.Name == "zitadel.quota.exhausted" {
if expectSet {
assert.Greater(t, cookie.MaxAge, 0)
} else {
assert.LessOrEqual(t, cookie.MaxAge, 0)
}
return
}
}
assert.FailNow(t, "cookie not found")
}
func assertGrpcError(t assert.TestingT, err error, expectBlocked bool) {
if expectBlocked {
assert.Equal(t, codes.ResourceExhausted, status.Convert(err).Code())
return
}
assert.NoError(t, err)
}
func assertLimitResponse(t assert.TestingT, response *http.Response, expectBlocked bool) {
if expectBlocked {
assert.Equal(t, http.StatusTooManyRequests, response.StatusCode)
return
}
assert.GreaterOrEqual(t, response.StatusCode, 200)
assert.Less(t, response.StatusCode, 300)
}
func idpExistsCondition(t assert.TestingT, instanceID, idpName string) func() bool {
return func() bool {
nameQuery, err := query.NewIDPTemplateNameSearchQuery(query.TextEquals, idpName)
assert.NoError(t, err)
instanceQuery, err := query.NewIDPTemplateResourceOwnerSearchQuery(instanceID)
assert.NoError(t, err)
idps, err := Tester.Queries.IDPTemplates(authz.WithInstanceID(CTX, instanceID), &query.IDPTemplateSearchQueries{
Queries: []query.SearchQuery{
instanceQuery,
nameQuery,
},
}, false)
assert.NoError(t, err)
return len(idps.Templates) > 0
}
}