diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index 11829eea4..eb040f11d 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -349,8 +349,10 @@ jobs: run: | kubectl port-forward --namespace everest-system deployment/everest-server 8080:8080 & - - name: Create Everest test user + - name: Create Everest test users run: | + ./bin/everestctl accounts create -u test -p password + echo "API_TOKEN_TEST=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "test","password": "password"}' | jq -r .token)" >> $GITHUB_ENV ./bin/everestctl accounts create -u everest_ci -p password echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "everest_ci","password": "password"}' | jq -r .token)" >> $GITHUB_ENV diff --git a/api-tests/tests/auth.spec.ts b/api-tests/tests/auth.spec.ts index 7a7ace10c..89709b62c 100644 --- a/api-tests/tests/auth.spec.ts +++ b/api-tests/tests/auth.spec.ts @@ -39,3 +39,25 @@ test.describe('no authorization header', () => { expect(version.status()).toEqual(400); }); }); + +test.describe('logout', () => { + test.use({ + extraHTTPHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Bearer ${process.env.API_TOKEN_TEST}`, + }, + }); + + test('authenticated api request fails after logout', async ({request}) => { + const versionBeforeLogout = await request.get('/v1/version'); + // NOTE: this test is сontext-dependent, the API_TOKEN_TEST needs to be valid before each run + expect(versionBeforeLogout.status()).toEqual(200); + + const response = await request.delete('/v1/session'); + expect(response.status()).toEqual(204); + + const versionAfterLogout = await request.get('/v1/version'); + expect(versionAfterLogout.status()).toEqual(401); + }); +}); diff --git a/api/everest-server.gen.go b/api/everest-server.gen.go index ed6ae4c47..6cc956bc0 100644 --- a/api/everest-server.gen.go +++ b/api/everest-server.gen.go @@ -1697,7 +1697,10 @@ type ServerInterface interface { // Cluster resources // (GET /resources) GetKubernetesClusterResources(ctx echo.Context) error - // Everest UI Login + // Everest API Logout + // (DELETE /session) + DeleteSession(ctx echo.Context) error + // Everest API Login // (POST /session) CreateSession(ctx echo.Context) error // Settings @@ -2519,6 +2522,17 @@ func (w *ServerInterfaceWrapper) GetKubernetesClusterResources(ctx echo.Context) return err } +// DeleteSession converts echo context to params. +func (w *ServerInterfaceWrapper) DeleteSession(ctx echo.Context) error { + var err error + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.DeleteSession(ctx) + return err +} + // CreateSession converts echo context to params. func (w *ServerInterfaceWrapper) CreateSession(ctx echo.Context) error { var err error @@ -2611,6 +2625,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.PATCH(baseURL+"/namespaces/:namespace/monitoring-instances/:name", wrapper.UpdateMonitoringInstance) router.GET(baseURL+"/permissions", wrapper.GetUserPermissions) router.GET(baseURL+"/resources", wrapper.GetKubernetesClusterResources) + router.DELETE(baseURL+"/session", wrapper.DeleteSession) router.POST(baseURL+"/session", wrapper.CreateSession) router.GET(baseURL+"/settings", wrapper.GetSettings) router.GET(baseURL+"/version", wrapper.VersionInfo) @@ -2618,12 +2633,12 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x9/XMbuZHov4JirmptH0nJ3t28i94PebLsbHS7XqskOVfvTL0YnAFJnGaACYCRzHX8", + "H4sIAAAAAAAC/+x9/XMbuZHov4Jiriq2j6Rk727erd4PebLsbHS7XqskOVfvTL0YnAFJnGaACYCRzHX8", "v79CN4D5wlBDfdhylqnKWpzB4KPR391AfxolMi+kYMLo0cGnkU5WLKfw50uaXJbFmZGKLpl9QNOUGy4F", "zU6ULJgynOnRwYJmmo1HKdOJ4oV9Pzpw3xKNHxMuFlLlFF6OR0Xt608jmmXymqW/0pzpgib4MGWFYgk1", "LB0dGFV2+v+Fa0PkgojwFXH9ECNJqRkxK67JvDGN0XjEDcthALMu2OhgpI3iYjn6PPYPqFJ0bX/Py+SS", "GTuraPPGdCLvF1Il7ISa1ZlZZwyXtKBlZgLA3CdzKTNGhf1G9A0WVtl9Ox59nCzlxD6c6EteTGSBWzQp", - "JBeGKYTf5/FIsWV0ssN7wO8+jZgo89HB+5H+fjQe0d9KxUYX4+6sS5VFV3PFFF+sz385a0AFd7kNFJj3", + "JBeGKYTf5/FIsWV0ssN7wO8+jZgo89HB+5H+bjQe0d9KxUYX4+6sS5VFV3PFFF+sz385a0AFd7kNFJj3", "P0quLCK8Rwg19sZ9Uo0v5//DEmPHaeCvthhjBwwY8G+KLUYHoz/sVQSw57B/r4n6Eew4Uowa1mh2QhXF", "nm9PJ4XtgxmmdJdMkoRp/TNbR2H6TRBRc/TzFSNJJss0rB5b7yVSGMoFU0TUdvhLEV9zkocWDIqkbMEF", "szO1Q8C8LODMitVYHPx89esZvkaGR1bGFPpgb++ynDMlmGF6yuVeKhNt15mwwug9ecXUFWfXe9dSXXKx", @@ -2641,102 +2656,102 @@ var swaggerSpec = []string{ "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", @@ -2752,62 +2767,63 @@ var swaggerSpec = []string{ "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", - "bORtLXXb9V09+nzx+f8HAAD//5OT678uSQEA", + "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", + "fP7/AQAA//+k8Tx8kUsBAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/client/everest-client.gen.go b/client/everest-client.gen.go index 9670c56e9..b94cc5b5b 100644 --- a/client/everest-client.gen.go +++ b/client/everest-client.gen.go @@ -1791,6 +1791,9 @@ type ClientInterface interface { // GetKubernetesClusterResources request GetKubernetesClusterResources(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // DeleteSession request + DeleteSession(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateSessionWithBody request with any body CreateSessionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -2367,6 +2370,18 @@ func (c *Client) GetKubernetesClusterResources(ctx context.Context, reqEditors . return c.Client.Do(req) } +func (c *Client) DeleteSession(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteSessionRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) CreateSessionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewCreateSessionRequestWithBody(c.Server, contentType, body) if err != nil { @@ -3941,6 +3956,33 @@ func NewGetKubernetesClusterResourcesRequest(server string) (*http.Request, erro return req, nil } +// NewDeleteSessionRequest generates requests for DeleteSession +func NewDeleteSessionRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/session") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewCreateSessionRequest calls the generic CreateSession builder with application/json body func NewCreateSessionRequest(server string, body CreateSessionJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -4208,6 +4250,9 @@ type ClientWithResponsesInterface interface { // GetKubernetesClusterResourcesWithResponse request GetKubernetesClusterResourcesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetKubernetesClusterResourcesResponse, error) + // DeleteSessionWithResponse request + DeleteSessionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DeleteSessionResponse, error) + // CreateSessionWithBodyWithResponse request with any body CreateSessionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateSessionResponse, error) @@ -5089,6 +5134,29 @@ func (r GetKubernetesClusterResourcesResponse) StatusCode() int { return 0 } +type DeleteSessionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON429 *Error + JSON500 *Error +} + +// Status returns HTTPResponse.Status +func (r DeleteSessionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteSessionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type CreateSessionResponse struct { Body []byte HTTPResponse *http.Response @@ -5096,6 +5164,7 @@ type CreateSessionResponse struct { Token *string `json:"token,omitempty"` } JSON400 *Error + JSON429 *Error JSON500 *Error } @@ -5571,6 +5640,15 @@ func (c *ClientWithResponses) GetKubernetesClusterResourcesWithResponse(ctx cont return ParseGetKubernetesClusterResourcesResponse(rsp) } +// DeleteSessionWithResponse request returning *DeleteSessionResponse +func (c *ClientWithResponses) DeleteSessionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DeleteSessionResponse, error) { + rsp, err := c.DeleteSession(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteSessionResponse(rsp) +} + // CreateSessionWithBodyWithResponse request with arbitrary body returning *CreateSessionResponse func (c *ClientWithResponses) CreateSessionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateSessionResponse, error) { rsp, err := c.CreateSessionWithBody(ctx, contentType, body, reqEditors...) @@ -7080,6 +7158,39 @@ func ParseGetKubernetesClusterResourcesResponse(rsp *http.Response) (*GetKuberne return response, nil } +// ParseDeleteSessionResponse parses an HTTP response from a DeleteSessionWithResponse call +func ParseDeleteSessionResponse(rsp *http.Response) (*DeleteSessionResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteSessionResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON429 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseCreateSessionResponse parses an HTTP response from a CreateSessionWithResponse call func ParseCreateSessionResponse(rsp *http.Response) (*CreateSessionResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -7110,6 +7221,13 @@ func ParseCreateSessionResponse(rsp *http.Response) (*CreateSessionResponse, err } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON429 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -7174,12 +7292,12 @@ func ParseVersionInfoResponse(rsp *http.Response) (*VersionInfoResponse, error) // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x9/XMbuZHov4JirmptH0nJ3t28i94PebLsbHS7XqskOVfvTL0YnAFJnGaACYCRzHX8", + "H4sIAAAAAAAC/+x9/XMbuZHov4Jiriq2j6Rk727erd4PebLsbHS7XqskOVfvTL0YnAFJnGaACYCRzHX8", "v79CN4D5wlBDfdhylqnKWpzB4KPR391AfxolMi+kYMLo0cGnkU5WLKfw50uaXJbFmZGKLpl9QNOUGy4F", "zU6ULJgynOnRwYJmmo1HKdOJ4oV9Pzpw3xKNHxMuFlLlFF6OR0Xt608jmmXymqW/0pzpgib4MGWFYgk1", "LB0dGFV2+v+Fa0PkgojwFXH9ECNJqRkxK67JvDGN0XjEDcthALMu2OhgpI3iYjn6PPYPqFJ0bX/Py+SS", "GTuraPPGdCLvF1Il7ISa1ZlZZwyXtKBlZgLA3CdzKTNGhf1G9A0WVtl9Ox59nCzlxD6c6EteTGSBWzQp", - "JBeGKYTf5/FIsWV0ssN7wO8+jZgo89HB+5H+fjQe0d9KxUYX4+6sS5VFV3PFFF+sz385a0AFd7kNFJj3", + "JBeGKYTf5/FIsWV0ssN7wO8+jZgo89HB+5H+bjQe0d9KxUYX4+6sS5VFV3PFFF+sz385a0AFd7kNFJj3", "P0quLCK8Rwg19sZ9Uo0v5//DEmPHaeCvthhjBwwY8G+KLUYHoz/sVQSw57B/r4n6Eew4Uowa1mh2QhXF", "nm9PJ4XtgxmmdJdMkoRp/TNbR2H6TRBRc/TzFSNJJss0rB5b7yVSGMoFU0TUdvhLEV9zkocWDIqkbMEF", "szO1Q8C8LODMitVYHPx89esZvkaGR1bGFPpgb++ynDMlmGF6yuVeKhNt15mwwug9ecXUFWfXe9dSXXKx", @@ -7197,102 +7315,102 @@ var swaggerSpec = []string{ "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", @@ -7308,62 +7426,63 @@ var swaggerSpec = []string{ "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", - "bORtLXXb9V09+nzx+f8HAAD//5OT678uSQEA", + "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", + "fP7/AQAA//+k8Tx8kUsBAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/docs/spec/openapi.yml b/docs/spec/openapi.yml index 3ffae329b..ddd78ff07 100644 --- a/docs/spec/openapi.yml +++ b/docs/spec/openapi.yml @@ -38,7 +38,7 @@ paths: tags: - Authentication & Authorization security: [] - summary: Everest UI Login + summary: Everest API Login description: | This API issues a new JWT token for logging in from the Everest API. The provided user must have the `login` capability. @@ -59,6 +59,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + '429': + description: Too many attempts + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '500': description: Internal server error content: @@ -72,6 +78,28 @@ paths: application/json: schema: $ref: '#/components/schemas/UserCredentials' + delete: + tags: + - Authentication & Authorization + summary: Everest API Logout + description: | + This API invalidates Everest API JWT token. + operationId: deleteSession + responses: + '204': + description: Successful operation + '429': + description: Too many attempts + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '/permissions': get: tags: diff --git a/internal/server/everest.go b/internal/server/everest.go index 7113ab045..2466d2c98 100644 --- a/internal/server/everest.go +++ b/internal/server/everest.go @@ -89,7 +89,7 @@ func getOIDCProviderConfig(ctx context.Context, kubeClient kubernetes.Kubernetes // NewEverestServer creates and configures everest API. func NewEverestServer(ctx context.Context, c *config.EverestConfig, l *zap.SugaredLogger) (*EverestServer, error) { - kubeConnector, err := kubernetes.NewInCluster(l) + kubeConnector, err := kubernetes.NewInCluster(l, ctx, nil) if err != nil { return nil, errors.Join(err, errors.New("failed creating Kubernetes client")) } @@ -98,7 +98,9 @@ func NewEverestServer(ctx context.Context, c *config.EverestConfig, l *zap.Sugar echoServer.Use(echomiddleware.RateLimiter(echomiddleware.NewRateLimiterMemoryStore(rate.Limit(c.APIRequestsRateLimit)))) middleware, store := sessionRateLimiter(c.CreateSessionRateLimit) echoServer.Use(middleware) + sessMgr, err := session.New( + ctx, l, session.WithAccountManager(kubeConnector.Accounts()), ) if err != nil { @@ -204,6 +206,12 @@ func (e *EverestServer) initHTTPServer(ctx context.Context) error { } apiGroup.Use(jwtMW) + blocklistMW, err := e.sessionMgr.BlocklistMiddleWare(newSkipperFunc) + if err != nil { + return err + } + apiGroup.Use(blocklistMW) + apiGroup.Use(e.checkOperatorUpgradeState) api.RegisterHandlers(apiGroup, e) @@ -365,7 +373,7 @@ func (e *EverestServer) getBodyFromContext(ctx echo.Context, into any) error { func sessionRateLimiter(limit int) (echo.MiddlewareFunc, *RateLimiterMemoryStore) { allButSession := func(c echo.Context) bool { - return c.Request().Method != echo.POST || c.Request().URL.Path != "/v1/session" + return c.Request().URL.Path != "/v1/session" } config := echomiddleware.DefaultRateLimiterConfig config.Skipper = allButSession diff --git a/internal/server/session.go b/internal/server/session.go index ece540f0d..e8f03b7ef 100644 --- a/internal/server/session.go +++ b/internal/server/session.go @@ -28,6 +28,7 @@ import ( "github.com/percona/everest/api" "github.com/percona/everest/pkg/accounts" + "github.com/percona/everest/pkg/common" ) const ( @@ -66,6 +67,25 @@ func (e *EverestServer) CreateSession(ctx echo.Context) error { return ctx.JSON(http.StatusOK, map[string]string{"token": jwtToken}) } +// DeleteSession invalidates the user token by adding it to the blocklist +func (e *EverestServer) DeleteSession(ctx echo.Context) error { + e.attemptsStore.IncreaseTimeout(ctx.RealIP()) + c := ctx.Request().Context() + token, err := common.ExtractToken(c) + if err != nil { + return err + } + err = e.sessionMgr.Block(c, token) + if err != nil { + e.l.Errorf("blocklist error: %v", err) + return ctx.JSON(http.StatusInternalServerError, api.Error{ + Message: pointer.To("Failed to logout user"), + }) + } + + return ctx.NoContent(http.StatusNoContent) +} + func sessionErrToHTTPRes(ctx echo.Context, err error) error { if errors.Is(err, accounts.ErrAccountNotFound) || errors.Is(err, accounts.ErrIncorrectPassword) { diff --git a/pkg/cli/upgrade/upgrade.go b/pkg/cli/upgrade/upgrade.go index 8e84b0b5a..e887853a2 100644 --- a/pkg/cli/upgrade/upgrade.go +++ b/pkg/cli/upgrade/upgrade.go @@ -102,7 +102,7 @@ func NewUpgrade(cfg *Config, l *zap.SugaredLogger) (*Upgrade, error) { var kubeClient kubernetes.KubernetesConnector if cfg.InCluster { - k, err := kubernetes.NewInCluster(cli.l) + k, err := kubernetes.NewInCluster(cli.l, nil, nil) if err != nil { return nil, fmt.Errorf("could not create in-cluster kubernetes client: %w", err) } diff --git a/pkg/common/accounts.go b/pkg/common/accounts.go index de9c64282..49a2f5d30 100644 --- a/pkg/common/accounts.go +++ b/pkg/common/accounts.go @@ -5,6 +5,9 @@ import ( "crypto/rand" "encoding/hex" "errors" + "fmt" + + "github.com/golang-jwt/jwt/v5" "github.com/percona/everest/pkg/accounts" ) @@ -40,3 +43,12 @@ func CreateInitialAdminAccount( // Create the admin account. return c.SetPassword(ctx, EverestAdminUser, pass, false) } + +// ExtractToken extracts token from context +func ExtractToken(ctx context.Context) (*jwt.Token, error) { + token, ok := ctx.Value(UserCtxKey).(*jwt.Token) + if !ok { + return nil, fmt.Errorf("failed to get token from context") + } + return token, nil +} diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 4147c70d3..30b4ba08b 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -69,6 +69,8 @@ const ( EverestAccountsSecretName = "everest-accounts" // EverestJWTSecretName is the name of the secret that holds JWT secret. EverestJWTSecretName = "everest-jwt" + // EverestBlocklistSecretName is the name of the secret that holds JWT blocklist. + EverestBlocklistSecretName = "everest-blocklist" // EverestJWTPrivateKeyFile is the path to the JWT private key. EverestJWTPrivateKeyFile = "/etc/jwt/id_rsa" // EverestJWTPublicKeyFile is the path to the JWT public key. diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index b34a4efa0..dcc9f7d9c 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -38,6 +38,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" everestv1alpha1 "github.com/percona/everest-operator/api/v1alpha1" @@ -117,7 +118,7 @@ func New(kubeconfigPath string, l *zap.SugaredLogger) (KubernetesConnector, erro restConfig.QPS = defaultQPSLimit restConfig.Burst = defaultBurstLimit - k8client, err := ctrlclient.New(restConfig, getKubernetesClientOptions()) + k8client, err := ctrlclient.New(restConfig, getKubernetesClientOptions(nil)) if err != nil { return nil, err } @@ -131,12 +132,28 @@ func New(kubeconfigPath string, l *zap.SugaredLogger) (KubernetesConnector, erro } // NewInCluster creates a new kubernetes client using incluster authentication. -func NewInCluster(l *zap.SugaredLogger) (KubernetesConnector, error) { +func NewInCluster(l *zap.SugaredLogger, ctx context.Context, cacheOptions *cache.Options) (KubernetesConnector, error) { restConfig := ctrl.GetConfigOrDie() restConfig.QPS = defaultQPSLimit restConfig.Burst = defaultBurstLimit - k8sclient, err := ctrlclient.New(restConfig, getKubernetesClientOptions()) + var k8sCache cache.Cache + var err error + if cacheOptions != nil { + k8sCache, err = cache.New(restConfig, *cacheOptions) + if err != nil { + panic(err) + } + go func() { + l.Info("starting incluster client cache") + if err := k8sCache.Start(ctx); err != nil { + l.Errorf("error starting incluster client cache: %s", err) + os.Exit(1) + } + }() + } + + k8sclient, err := ctrlclient.New(restConfig, getKubernetesClientOptions(k8sCache)) if err != nil { return nil, err } @@ -163,10 +180,17 @@ func CreateScheme() *runtime.Scheme { return scheme } -func getKubernetesClientOptions() ctrlclient.Options { +func getKubernetesClientOptions(cache cache.Cache) ctrlclient.Options { + var cacheOptions *ctrlclient.CacheOptions + if cache != nil { + cacheOptions = &ctrlclient.CacheOptions{ + Reader: cache, + } + } + return ctrlclient.Options{ Scheme: CreateScheme(), - Cache: nil, // disable cache + Cache: cacheOptions, } } diff --git a/pkg/session/blocklist.go b/pkg/session/blocklist.go new file mode 100644 index 000000000..ab1da607b --- /dev/null +++ b/pkg/session/blocklist.go @@ -0,0 +1,170 @@ +// everest +// Copyright (C) 2025 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package session + +import ( + "context" + "errors" + "fmt" + "strconv" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/golang-jwt/jwt/v5" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/percona/everest/pkg/common" + "github.com/percona/everest/pkg/kubernetes" +) + +const ( + dataKey = "list" + maxRetries = 10 + backoffInterval = 500 * time.Millisecond +) + +var ( + errExtractJti = errors.New("could not extract jti") + errExtractExp = errors.New("could not extract exp") + errEmptyToken = errors.New("token is empty") + errUnsupportedClaim = func(claims any) error { + return errors.New(fmt.Sprintf("unsupported claims type: %T", claims)) + } +) + +// Blocklist represents interface to block JWT tokens and check if a token is blocked. +type Blocklist interface { + // Block invalidates the token from the context by adding it to blocklist. + Block(ctx context.Context, token *jwt.Token) error + // IsBlocked checks if the token from the context is blocked. + IsBlocked(ctx context.Context, token *jwt.Token) (bool, error) +} + +type blocklist struct { + tokenStore TokenStore + l *zap.SugaredLogger +} + +// TokenStore represents an abstraction for storage, hiding details about how the data is actually stored. +type TokenStore interface { + // Add adds the shortened token to the blocklist + Add(ctx context.Context, shortenedToken string) error + // Exists checks if the shortened token is in the blocklist + Exists(ctx context.Context, shortenedToken string) (bool, error) +} + +// NewBlocklist creates a new block list +func NewBlocklist(ctx context.Context, logger *zap.SugaredLogger) (Blocklist, error) { + options := &cache.Options{ + ByObject: map[client.Object]cache.ByObject{ + &corev1.Secret{}: { + Field: fields.SelectorFromSet(fields.Set{"metadata.name": common.EverestBlocklistSecretName}), + }, + }, + } + // A separate client is needed to apply the controller-runtime cache only to the related objects. + // Using the controller-runtime client is also beneficial because it supports HA mode. + tokenStoreClient, err := kubernetes.NewInCluster(logger, ctx, options) + if err != nil { + return nil, errors.Join(err, errors.New("failed creating Kubernetes client for blockList")) + } + + store, err := newTokenStore(ctx, tokenStoreClient, logger) + if err != nil { + return nil, err + } + return &blocklist{ + tokenStore: store, + l: logger, + }, nil +} + +// Block invalidates the token from the context by adding it to blocklist. +func (b *blocklist) Block(ctx context.Context, token *jwt.Token) error { + shortenedToken, err := shortenToken(token) + if err != nil { + return err + } + + var bOff backoff.BackOff + bOff = backoff.NewConstantBackOff(backoffInterval) + bOff = backoff.WithMaxRetries(bOff, maxRetries) + bOff = backoff.WithContext(bOff, ctx) + return backoff.Retry( + func() error { + return b.tokenStore.Add(ctx, shortenedToken) + }, + bOff, + ) +} + +// IsBlocked checks if the token from the context is blocked. +func (b *blocklist) IsBlocked(ctx context.Context, token *jwt.Token) (bool, error) { + shortenedToken, err := shortenToken(token) + if err != nil { + return false, fmt.Errorf("failed to shorten token: %w", err) + } + + return b.tokenStore.Exists(ctx, shortenedToken) +} + +// shortenToken contains only the "jti" and the "exp" claims from the token, so the format of shortened token is +// , for example "9d1c1f98-a479-41e3-8939-c7cb3edefa331743679478", +// where last 10 digits represent the expiration timestamp. +func shortenToken(token *jwt.Token) (string, error) { + content, err := extractContent(token) + if err != nil { + return "", err + } + jti, ok := content.Payload["jti"].(string) + if !ok { + return "", errExtractJti + } + exp, ok := content.Payload["exp"].(float64) + if !ok { + return "", errExtractExp + } + return jti + strconv.FormatFloat(exp, 'f', 0, 64), nil +} + +// JWTContent represents the JWT token structure that is used by blocklist. +type JWTContent struct { + Payload map[string]interface{} `json:"payload"` +} + +func extractContent(token *jwt.Token) (*JWTContent, error) { + if token == nil { + return nil, errEmptyToken + } + claimsMap := make(map[string]interface{}) + + switch claims := token.Claims.(type) { + case jwt.MapClaims: + for key, val := range claims { + claimsMap[key] = val + } + default: + return nil, errUnsupportedClaim(claims) + } + + return &JWTContent{ + Payload: claimsMap, + }, nil +} diff --git a/pkg/session/blocklist_test.go b/pkg/session/blocklist_test.go new file mode 100644 index 000000000..32b5bc454 --- /dev/null +++ b/pkg/session/blocklist_test.go @@ -0,0 +1,221 @@ +// everest +// Copyright (C) 2025 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package session + +import ( + "context" + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/percona/everest/pkg/common" + "github.com/percona/everest/pkg/kubernetes" +) + +func TestShortenToken(t *testing.T) { + type tcase struct { + name string + claims jwt.MapClaims + shortenedToken string + error error + } + tcases := []tcase{ + { + name: "valid", + claims: jwt.MapClaims{ + "jti": "9d1c1f98-a479-41e3-8939-c7cb3edefa", + "exp": float64(331743679478), + }, + shortenedToken: "9d1c1f98-a479-41e3-8939-c7cb3edefa331743679478", + error: nil, + }, + { + name: "no jti", + claims: jwt.MapClaims{ + "exp": float64(331743679478), + }, + shortenedToken: "", + error: errExtractJti, + }, + { + name: "no exp", + claims: jwt.MapClaims{ + "jti": "9d1c1f98-a479-41e3-8939-c7cb3e049a", + }, + shortenedToken: "", + error: errExtractExp, + }, + } + for _, tc := range tcases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result, err := shortenToken(jwt.NewWithClaims(jwt.SigningMethodHS256, tc.claims)) + assert.Equal(t, tc.error, err) + assert.Equal(t, tc.shortenedToken, result) + }) + } +} + +func TestExtractContent(t *testing.T) { + type tcase struct { + name string + token *jwt.Token + error error + result *JWTContent + } + tokenUnsupportedClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{}) + tcases := []tcase{ + { + name: "empty token", + token: nil, + result: nil, + error: errEmptyToken, + }, + { + name: "unsupported claims", + token: tokenUnsupportedClaims, + result: nil, + error: errUnsupportedClaim(tokenUnsupportedClaims.Claims), + }, + { + name: "valid empty payload", + token: jwt.New(jwt.SigningMethodHS256), + result: &JWTContent{ + Payload: make(map[string]interface{}), + }, + error: nil, + }, + { + name: "valid with payload", + token: jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"jti": "9d1c1f98-a479-41e3-8939-c7cb3e049a", "exp": float64(331743679478)}), + result: &JWTContent{ + Payload: map[string]interface{}{"exp": float64(331743679478), "jti": "9d1c1f98-a479-41e3-8939-c7cb3e049a"}, + }, + error: nil, + }, + } + for _, tc := range tcases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result, err := extractContent(tc.token) + assert.Equal(t, tc.error, err) + assert.Equal(t, tc.result, result) + }) + } +} + +func TestBlocklist_Block(t *testing.T) { + mockClient := fakeclient.NewClientBuilder().WithScheme(kubernetes.CreateScheme()) + l := zap.NewNop().Sugar() + k := kubernetes.NewEmpty(l).WithKubernetesClient(mockClient.Build()) + + ctx := context.Background() + jwt := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"jti": "9d1c1f98-a479-41e3-8939-c7cb3e049a", "exp": float64(331743679478)}) + + // check there is no blocklist secret before the blocklist creation + secret, err := k.GetSecret(ctx, ctrlclient.ObjectKey{ + Name: common.EverestBlocklistSecretName, + Namespace: common.SystemNamespace, + }) + assert.True(t, k8serrors.IsNotFound(err)) + assert.Nil(t, secret) + + b, err := mockNewBlocklist(ctx, l, k) + assert.NoError(t, err) + + // blocklist secret appears after the blocklist creation + secret, err = k.GetSecret(ctx, ctrlclient.ObjectKey{ + Name: common.EverestBlocklistSecretName, + Namespace: common.SystemNamespace, + }) + assert.NoError(t, err) + assert.NotNil(t, secret) + assert.Equal(t, "", secret.StringData[dataKey]) + + // block the token from the context and check the secret has been changed accordingly + err = b.Block(ctx, jwt) + assert.NoError(t, err) + + secret, err = k.GetSecret(ctx, ctrlclient.ObjectKey{ + Name: common.EverestBlocklistSecretName, + Namespace: common.SystemNamespace, + }) + assert.NoError(t, err) + // the mocked client does not do this StringData -> Data transformation in Secrets which the actual k8a API do, so + // we only check the StringData field + assert.Equal(t, "9d1c1f98-a479-41e3-8939-c7cb3e049a331743679478", secret.StringData[dataKey]) + + // deleting secret to test the backoff + err = k.DeleteSecret(ctx, secret) + assert.NoError(t, err) + + // after deleting secret - try to block again, get the NotFound error + err = b.Block(ctx, jwt) + assert.Equal(t, true, k8serrors.IsNotFound(err)) +} + +func TestBlocklist_IsBlocked(t *testing.T) { + secret := getBlockListSecretTemplate("the-blocked-jti331743679478") + // when writing Secrets, the mocked client does not do this StringData -> Data transformation which the actual k8a API do, + // so we set the Data field manually + secret.Data = map[string][]byte{dataKey: []byte("the-blocked-jti331743679478")} + objs := []ctrlclient.Object{secret} + + mockClient := fakeclient.NewClientBuilder().WithScheme(kubernetes.CreateScheme()) + mockClient.WithObjects(objs...) + l := zap.NewNop().Sugar() + k := kubernetes.NewEmpty(l).WithKubernetesClient(mockClient.Build()) + + t.Run("blocked token in context", func(t *testing.T) { + ctx := context.Background() + jwt := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"jti": "the-blocked-jti", "exp": float64(331743679478)}) + + b, err := mockNewBlocklist(ctx, l, k) + assert.NoError(t, err) + + blocked, err := b.IsBlocked(ctx, jwt) + assert.NoError(t, err) + assert.True(t, blocked) + }) + + t.Run("not blocked token in context", func(t *testing.T) { + ctx := context.Background() + + b, err := mockNewBlocklist(ctx, l, k) + assert.NoError(t, err) + + jwt := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"jti": "some-other-jti", "exp": float64(331743679478)}) + blocked, err := b.IsBlocked(ctx, jwt) + assert.NoError(t, err) + assert.False(t, blocked) + }) +} + +func mockNewBlocklist(ctx context.Context, logger *zap.SugaredLogger, mockClient TokenStoreClient) (Blocklist, error) { + store, err := newTokenStore(ctx, mockClient, logger) + if err != nil { + return nil, err + } + return &blocklist{ + tokenStore: store, + l: logger, + }, nil +} diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 6ca541b03..7547e1361 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -23,11 +23,17 @@ import ( "encoding/pem" "errors" "fmt" + "net/http" "os" "time" + "github.com/AlekSi/pointer" "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v4" + echomiddleware "github.com/labstack/echo/v4/middleware" + "go.uber.org/zap" + "github.com/percona/everest/api" "github.com/percona/everest/pkg/accounts" "github.com/percona/everest/pkg/common" ) @@ -41,13 +47,15 @@ const ( type Manager struct { accountManager accounts.Interface signingKey *rsa.PrivateKey + Blocklist + l *zap.SugaredLogger } // Option is a function that modifies a SessionManager. type Option func(*Manager) // New creates a new session manager with the given options. -func New(options ...Option) (*Manager, error) { +func New(ctx context.Context, l *zap.SugaredLogger, options ...Option) (*Manager, error) { m := &Manager{} for _, opt := range options { opt(m) @@ -57,6 +65,14 @@ func New(options ...Option) (*Manager, error) { return nil, errors.Join(err, errors.New("failed to get private key")) } m.signingKey = privKey + m.l = l + + blockList, err := NewBlocklist(ctx, l) + if err != nil { + return nil, errors.Join(err, errors.New("failed to configure tokens blocklist")) + } + + m.Blocklist = blockList return m, nil } @@ -135,3 +151,31 @@ func (mgr *Manager) KeyFunc() jwt.Keyfunc { return mgr.signingKey.Public(), nil } } + +func (mgr *Manager) BlocklistMiddleWare(skipperFunc func() (echomiddleware.Skipper, error)) (echo.MiddlewareFunc, error) { + skipper, err := skipperFunc() + if err != nil { + return nil, err + } + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if skipper(c) { + return next(c) + } + ctx := c.Request().Context() + token, tErr := common.ExtractToken(ctx) + if tErr != nil { + return tErr + } + if isBlocked, err := mgr.IsBlocked(ctx, token); err != nil { + mgr.l.Error(err) + return err + } else if isBlocked { + return c.JSON(http.StatusUnauthorized, api.Error{ + Message: pointer.ToString("Invalid token"), + }) + } + return next(c) + } + }, nil +} diff --git a/pkg/session/token_store.go b/pkg/session/token_store.go new file mode 100644 index 000000000..7cb6bb0e6 --- /dev/null +++ b/pkg/session/token_store.go @@ -0,0 +1,170 @@ +// everest +// Copyright (C) 2025 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package session + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/percona/everest/pkg/common" +) + +const ( + sep = "," + timestampLen = 10 +) + +// TokenStoreClient contains the methods that are needed for the token store management. +type TokenStoreClient interface { + // GetSecret returns a secret that matches the criteria. + GetSecret(ctx context.Context, key client.ObjectKey) (*corev1.Secret, error) + // CreateSecret creates a secret. + CreateSecret(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) + // UpdateSecret updates a secret. + UpdateSecret(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) +} + +func newTokenStore(ctx context.Context, client TokenStoreClient, logger *zap.SugaredLogger) (TokenStore, error) { + s := &tokenStore{ + l: logger, + client: client, + } + err := s.init(ctx) + if err != nil { + return nil, err + } + return s, nil +} + +func (ts *tokenStore) init(ctx context.Context) error { + _, err := ts.client.GetSecret(ctx, types.NamespacedName{Namespace: common.SystemNamespace, Name: common.EverestBlocklistSecretName}) + if err == nil { + return err + } + if !k8serrors.IsNotFound(err) { + err = fmt.Errorf("failed to get %s secret in the %s namespace: %w", common.EverestBlocklistSecretName, common.SystemNamespace, err) + ts.l.Error(err) + return err + } + var createErr error + secret := getBlockListSecretTemplate("") + _, createErr = ts.client.CreateSecret(ctx, secret) + if createErr != nil { + err = fmt.Errorf("failed to create secret %s in namespace %s: %w", secret.Name, secret.Namespace, createErr) + ts.l.Error(err) + return err + } + return nil +} + +type tokenStore struct { + client TokenStoreClient + l *zap.SugaredLogger +} + +// Add adds the shortened token to the blocklist +func (ts *tokenStore) Add(ctx context.Context, shortenedToken string) error { + secret, err := ts.client.GetSecret(ctx, types.NamespacedName{Namespace: common.SystemNamespace, Name: common.EverestBlocklistSecretName}) + if err != nil { + ts.l.Errorf("failed to get %s secret in the %s namespace: %v", common.EverestBlocklistSecretName, common.SystemNamespace, err) + return err + } + + secret = addDataToSecret(ts.l, secret, shortenedToken, time.Now().UTC()) + _, updateErr := ts.client.UpdateSecret(ctx, secret) + if updateErr != nil { + ts.l.Errorf("failed to update %s secret in the %s namespace with the %s shortened token, retrying: %v", secret.Name, secret.Namespace, shortenedToken, updateErr) + return err + } + return nil +} + +// Exists checks if the shortened token is in the blocklist +func (ts *tokenStore) Exists(ctx context.Context, shortenedToken string) (bool, error) { + // no worries about overwhelming k8s API - the secret is cached + secret, err := ts.client.GetSecret(ctx, types.NamespacedName{Namespace: common.SystemNamespace, Name: common.EverestBlocklistSecretName}) + if err != nil { + ts.l.Errorf("failed to get %s secret in the %s namespace: %v", common.EverestBlocklistSecretName, common.SystemNamespace, err) + return false, err + } + list, ok := secret.Data[dataKey] + return ok && strings.Contains(string(list), shortenedToken), nil +} + +func addDataToSecret(l *zap.SugaredLogger, secret *corev1.Secret, shortenedToken string, now time.Time) *corev1.Secret { + existingList, ok := secret.Data[dataKey] + if !ok { + secret.StringData = map[string]string{ + dataKey: shortenedToken, + } + return secret + } + list := append(cleanupOld(l, string(existingList), now), shortenedToken) + secret.StringData = map[string]string{ + dataKey: strings.Join(list, sep), + } + return secret +} + +func cleanupOld(l *zap.SugaredLogger, list string, now time.Time) []string { + tokens := strings.Split(list, sep) + newList := make([]string, 0, len(tokens)) + for _, shortenedToken := range tokens { + length := len(shortenedToken) + if length < timestampLen { + l.Warnf("blocklist token='%s' contains irregular data format", shortenedToken) + continue + } + ts := shortenedToken[length-10 : length] + tsInt, err := strconv.ParseInt(ts, 10, 64) + if err != nil { + l.Warnf("failed to parse timestamp %v", tsInt) + continue + } + timeObj := time.Unix(tsInt, 0) + // only keep the tokens which natural expiration time is not over yet + if timeObj.After(now) { + newList = append(newList, shortenedToken) + } + } + return newList +} + +func getBlockListSecretTemplate(stringData string) *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: common.EverestBlocklistSecretName, + Namespace: common.SystemNamespace, + }, + StringData: map[string]string{ + dataKey: stringData, + }, + } +} diff --git a/pkg/session/token_store_test.go b/pkg/session/token_store_test.go new file mode 100644 index 000000000..6a51c86eb --- /dev/null +++ b/pkg/session/token_store_test.go @@ -0,0 +1,247 @@ +// everest +// Copyright (C) 2025 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package session + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" + corev1 "k8s.io/api/core/v1" +) + +func TestCleanupOld(t *testing.T) { + type tcase struct { + name string + data string + thresholdDate time.Time + expected []string + } + l := zaptest.NewLogger(t).Sugar() + tcases := []tcase{ + { + name: "one outdated", + data: "id123AAA1743687192,id2323BBB1743687194", + thresholdDate: time.Date(2025, 4, 3, 13, 33, 13, 0, time.UTC), + expected: []string{"id2323BBB1743687194"}, + }, + { + name: "two outdated", + data: "id123AAA1743687192,id2323BBB1743687193,id2323BBB1743687194", + thresholdDate: time.Date(2025, 4, 3, 13, 33, 13, 0, time.UTC), + expected: []string{"id2323BBB1743687194"}, + }, + { + name: "all outdated", + data: "id123AAA1743687191,id2323BBB1743687192,id2323BBB1743687193", + thresholdDate: time.Date(2025, 4, 3, 13, 33, 13, 0, time.UTC), + expected: []string{}, + }, + { + name: "all fresh", + data: "id123AAA1743687194,id2323BBB1743687195,id2323BBB1743687196", + thresholdDate: time.Date(2025, 4, 3, 13, 33, 13, 0, time.UTC), + expected: []string{"id123AAA1743687194", "id2323BBB1743687195", "id2323BBB1743687196"}, + }, + } + + for _, tc := range tcases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.expected, cleanupOld(l, tc.data, tc.thresholdDate)) + }) + } +} + +func TestAddDataToSecret(t *testing.T) { + type tcase struct { + name string + data string + secret *corev1.Secret + thresholdDate time.Time + expected *corev1.Secret + } + l := zaptest.NewLogger(t).Sugar() + tcases := []tcase{ + { + name: "empty secret", + data: "id123AAA1743687192", + secret: &corev1.Secret{}, + thresholdDate: time.Date(2025, 4, 3, 13, 33, 10, 0, time.UTC), + expected: &corev1.Secret{ + StringData: map[string]string{ + dataKey: "id123AAA1743687192", + }, + }, + }, + { + name: "nothing to delete, add newer data", + data: "id123AAA1743687192", + secret: &corev1.Secret{ + Data: map[string][]byte{ + dataKey: []byte("id123AAA1743687191"), + }, + }, + thresholdDate: time.Date(2025, 4, 3, 13, 33, 10, 0, time.UTC), + expected: &corev1.Secret{ + // the Data field is updated only when the object is applied to k8s, so for this test + // only the write-only StringData field is expected to be changed. + Data: map[string][]byte{ + dataKey: []byte("id123AAA1743687191"), + }, + StringData: map[string]string{ + dataKey: "id123AAA1743687191,id123AAA1743687192", + }, + }, + }, + { + name: "deleted old data, add newer data", + data: "id123AAA1743687194", + secret: &corev1.Secret{ + Data: map[string][]byte{ + dataKey: []byte("id123AAA1743687191,id123AAA1743687193"), + }, + }, + thresholdDate: time.Date(2025, 4, 3, 13, 33, 12, 0, time.UTC), + expected: &corev1.Secret{ + // the Data field is updated only when the object is applied to k8s, so for this test + // only the write-only StringData field is expected to be changed. + Data: map[string][]byte{ + dataKey: []byte("id123AAA1743687191,id123AAA1743687193"), + }, + StringData: map[string]string{ + dataKey: "id123AAA1743687193,id123AAA1743687194", + }, + }, + }, + { + name: "deleted all old data, add newer data", + data: "id123AAA1743687195", + secret: &corev1.Secret{ + Data: map[string][]byte{ + dataKey: []byte("id123AAA1743687191,id123AAA1743687193"), + }, + }, + thresholdDate: time.Date(2025, 4, 3, 13, 33, 14, 0, time.UTC), + expected: &corev1.Secret{ + // the Data field is updated only when the object is applied to k8s, so for this test + // only the write-only StringData field is expected to be changed. + Data: map[string][]byte{ + dataKey: []byte("id123AAA1743687191,id123AAA1743687193"), + }, + StringData: map[string]string{ + dataKey: "id123AAA1743687195", + }, + }, + }, + } + + for _, tc := range tcases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result := addDataToSecret(l, tc.secret, tc.data, tc.thresholdDate) + assert.Equal(t, tc.expected, result) + }) + } +} + +/* +This benchmark measures how much time does it take to clean up a long lists of tokens. +On an Apple M3 Pro it takes ~0.67 ms to perform a cleanup for a list of 10,000 tokens, which is acceptable. + +goos: darwin +goarch: arm64 +pkg: github.com/percona/everest/pkg/session +cpu: Apple M3 Pro +BenchmarkCleanupOld +BenchmarkCleanupOld-12 1500 671899 ns/op +*/ +func BenchmarkCleanupOld(b *testing.B) { + numTokens := 10000 + list := generateTestList(numTokens) + thresholdDate := time.Date(2025, 4, 3, 13, 33, 1, 0, time.UTC) + l := zap.L().Sugar() + b.ResetTimer() + for i := 0; i < b.N; i++ { + cleanupOld(l, list, thresholdDate) + } +} + +func generateTestList(numTokens int) string { + var builder strings.Builder + expDate := time.Date(2070, 4, 3, 13, 33, 1, 0, time.UTC).Unix() + for i := 0; i < numTokens; i++ { + // expiration date is year 2070 which is long ahead, so all the tokens should be kept + builder.WriteString("21669bd9-2374-4dc1-9238-77d5cad01fed" + fmt.Sprintf("%d", expDate)) + if i < numTokens-1 { + builder.WriteString(sep) + } + } + return builder.String() +} + +const ( + stringSizeMB = 1 // Size of the string in MB +) + +// generateLargeString creates a string of the specified size in MB. +func generateLargeString(sizeMB int) string { + size := sizeMB * 1024 * 1024 // Convert MB to bytes + builder := strings.Builder{} + builder.Grow(size) + + // Repeat a simple pattern to fill the string. This is important. + // Random data will compress differently and may not represent realistic + // data patterns. + pattern := "abcdefghijklmnopqrstuvwxyz" + patternLen := len(pattern) + + for i := 0; i < size; i++ { + builder.WriteByte(pattern[i%patternLen]) + } + + return builder.String() +} + +// BenchmarkStringsContains measures the time it takes to run strings.Contains on a 1MB string. +func BenchmarkStringsContains(b *testing.B) { + largeString := generateLargeString(stringSizeMB) + // Substring to search for. Choose a substring that's *likely* to be found for a realistic test. + substring := "xyz" + + // Reset the timer to exclude the setup time. + b.ResetTimer() + + for i := 0; i < b.N; i++ { + strings.Contains(largeString, substring) + } +} + +// BenchmarkStringsContainsNotFound measures the time it takes to run strings.Contains on a 1MB string when the substring is NOT present. +func BenchmarkStringsContainsNotFound(b *testing.B) { + largeString := generateLargeString(stringSizeMB) + substring := "thisstringisdefinitelynotpresent" // Substring that is NOT present. + + b.ResetTimer() // Reset the timer to exclude the setup time. + + for i := 0; i < b.N; i++ { + strings.Contains(largeString, substring) + } +}