Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion internal/dashboard/business/oceanbase/obtenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,11 +788,14 @@ func DeleteSQLAnalyzerDeployment(ctx context.Context, tenant *v1alpha1.OBTenant)
}

func ListAllOBTenants(ctx context.Context, ns string, listOptions v1.ListOptions) ([]*response.OBTenantOverview, error) {
username, ok := ctx.Value("username").(string)
if !ok || username == "" {
return nil, oberr.NewUnauthorized("login required")
}
tenantList, err := clients.ListAllOBTenants(ctx, ns, listOptions)
if err != nil {
return nil, err
}
username := ctx.Value("username").(string)
tenantList = filterTenants(username, "read", tenantList)
sort.Slice(tenantList.Items, func(i, j int) bool {
return tenantList.Items[i].Name < tenantList.Items[j].Name
Expand Down
84 changes: 84 additions & 0 deletions internal/dashboard/business/oceanbase/obtenant_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
Copyright (c) 2026 OceanBase
ob-operator is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
*/

package oceanbase

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"

"github.com/oceanbase/ob-operator/api/v1alpha1"
"github.com/oceanbase/ob-operator/internal/clients"
oberr "github.com/oceanbase/ob-operator/pkg/errors"
"github.com/oceanbase/ob-operator/pkg/k8s/client"
)

type authenticationTenantClient struct {
client.K8sResourceClient[*v1alpha1.OBTenant]
listCalls int
}

func (c *authenticationTenantClient) List(_ context.Context, _ string, list runtimeclient.ObjectList, _ metav1.ListOptions) error {
c.listCalls++
list.(*v1alpha1.OBTenantList).Items = nil
return nil
}

func TestListAllOBTenantsRequiresIdentity(t *testing.T) {
for _, tc := range []struct {
name string
username interface{}
}{
{"missing", nil},
{"empty", ""},
{"wrong type", 123},
} {
t.Run(tc.name, func(t *testing.T) {
fake := &authenticationTenantClient{}
previous := clients.TenantClient
clients.TenantClient = fake
t.Cleanup(func() { clients.TenantClient = previous })
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
if tc.username != nil {
ctx.Set("username", tc.username)
}
require.NotPanics(t, func() {
tenants, err := ListAllOBTenants(ctx, "test", metav1.ListOptions{})
require.Nil(t, tenants)
var authErr oberr.ObError
require.ErrorAs(t, err, &authErr)
require.Equal(t, http.StatusUnauthorized, authErr.Status())
})
require.Zero(t, fake.listCalls, "reject missing identity before reading tenant data")
})
}
}

func TestListAllOBTenantsWithIdentity(t *testing.T) {
fake := &authenticationTenantClient{}
previous := clients.TenantClient
clients.TenantClient = fake
t.Cleanup(func() { clients.TenantClient = previous })
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Set("username", "test-user")
tenants, err := ListAllOBTenants(ctx, "test", metav1.ListOptions{})
require.NoError(t, err)
require.Empty(t, tenants)
require.Equal(t, 1, fake.listCalls)
}
38 changes: 38 additions & 0 deletions internal/dashboard/handler/obtenant_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright (c) 2026 OceanBase
ob-operator is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
*/

package handler

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)

func TestListAllTenantsPreservesUnauthorized(t *testing.T) {
// Exercise the handler and response wrapper without LoginRequired, so a
// business-layer authentication error must itself remain HTTP 401.
gin.SetMode(gin.TestMode)
router := gin.New()
router.GET("/api/v1/obtenants", Wrap(ListAllTenants))
for _, uri := range []string{"/api/v1/obtenants?ns=info", "/api/v1/obtenants?obcluster=example"} {
t.Run(uri, func(t *testing.T) {
response := httptest.NewRecorder()
router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, uri, nil))
require.Equal(t, http.StatusUnauthorized, response.Code, response.Body.String())
require.JSONEq(t, `{"data":null,"message":"Error Unauthorized: login required","successful":false}`, response.Body.String())
})
}
}
4 changes: 2 additions & 2 deletions internal/dashboard/handler/obtenant_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ func ListAllTenants(c *gin.Context) ([]*response.OBTenantOverview, error) {
}
tenants, err := oceanbase.ListAllOBTenants(c, ns, listOptions)
if err != nil {
return nil, httpErr.NewInternal(err.Error())
return nil, err
}
if len(tenants) == 0 && c.Query("obcluster") != "" {
allTenants, err := oceanbase.ListAllOBTenants(c, ns, metav1.ListOptions{})
if err != nil {
return nil, httpErr.NewInternal(err.Error())
return nil, err
}
for i := range allTenants {
if allTenants[i].ClusterName == c.Query("obcluster") {
Expand Down
33 changes: 21 additions & 12 deletions internal/dashboard/middleware/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ See the Mulan PSL v2 for more details.
package middleware

import (
"strings"
"net/http"
"time"

"github.com/gin-contrib/sessions"
Expand All @@ -26,25 +26,34 @@ import (

// authentication

// Match registered routes, not request text: query values and path parameters
// must never make a protected endpoint anonymous.
func isAnonymousRoute(c *gin.Context) bool {
switch c.FullPath() {
case "/api/v1/info", "/api/v1/monitor/endpoints":
return c.Request.Method == http.MethodGet
case "/api/v1/login", "/api/v1/webhook/alert/log", "/api/v1/auth/:token":
return c.Request.Method == http.MethodPost
default:
return false
}
}

func LoginRequired() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.HasSuffix(c.Request.RequestURI, "login") ||
strings.HasSuffix(c.Request.RequestURI, "info") ||
strings.HasSuffix(c.Request.RequestURI, "monitor/endpoints") ||
strings.HasSuffix(c.Request.RequestURI, "webhook/alert/log") ||
strings.HasPrefix(c.Request.RequestURI, "/api/v1/auth/") {
if isAnonymousRoute(c) {
c.Next()
return
}
session := sessions.Default(c)
if session.Get("username") == nil {
username, ok := session.Get("username").(string)
if !ok || username == "" {
c.AbortWithStatusJSON(401, gin.H{
"message": "login required",
})
return
}

username := session.Get("username").(string)
_, exist := store.GetCache().Load(username)
if !exist {
c.AbortWithStatusJSON(401, gin.H{
Expand All @@ -53,14 +62,14 @@ func LoginRequired() gin.HandlerFunc {
return
}

expr := session.Get("expiration")
if expr == nil || expr.(int64) < 0 {
expr, ok := session.Get("expiration").(int64)
if !ok || expr < 0 {
c.AbortWithStatusJSON(403, gin.H{
"message": "cookie broken",
})
return
}
expriration := time.Unix(expr.(int64), 0)
expriration := time.Unix(expr, 0)
if expriration.Before(time.Now()) {
session.Clear()
session.Options(sessions.Options{Path: "/", MaxAge: -1}) // this sets the cookie with a MaxAge of 0
Expand All @@ -84,7 +93,7 @@ func LoginRequired() gin.HandlerFunc {

func RefreshExpiration() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.HasSuffix(c.Request.RequestURI, "login") || strings.HasSuffix(c.Request.RequestURI, "info") {
if isAnonymousRoute(c) {
c.Next()
return
}
Expand Down
Loading
Loading