Skip to content

Commit

Permalink
ui, server: add multitenant login/logout and tenant dropdown
Browse files Browse the repository at this point in the history
This patch enables login/logout for all tenants on the cluster
by fanning out the incoming requests to each tenant server.
Multitenant login changes the format of the session cookie
as <session>,<tenant_name&<session2>,<tenant_name2> etc.
The admin ui displays a dropdown with a list of tenants
the user has successfully logged in to. Selecting a different
tenant sets the tenant cookie to the selected tenant name
and reloads the page. If the cluster is not multitenant, the
dropdown will not display.

Release note (ui change): added a top-level dropdown
on the admin ui which lists tenants the user has logged
in to. If not multitenant, the dropdown is not displayed.
  • Loading branch information
Santamaura committed Dec 5, 2022
1 parent 30a1390 commit cb7c30a
Show file tree
Hide file tree
Showing 15 changed files with 578 additions and 23 deletions.
1 change: 1 addition & 0 deletions pkg/server/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ go_library(
"server_http.go",
"server_sql.go",
"server_systemlog_gc.go",
"session_writer.go",
"settings_cache.go",
"sql_stats.go",
"start_listen.go",
Expand Down
13 changes: 12 additions & 1 deletion pkg/server/api_v2_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"context"
"encoding/base64"
"net/http"
"strings"

"github.com/cockroachdb/cockroach/pkg/base"
"github.com/cockroachdb/cockroach/pkg/security/username"
Expand Down Expand Up @@ -325,7 +326,17 @@ func (a *authenticationV2Mux) getSession(
var decoded []byte
var err error
for i := range possibleSessions {
decoded, err = base64.StdEncoding.DecodeString(possibleSessions[i])
var session string
// This case is if the session cookie has a multi-tenant pattern.
if strings.Contains(possibleSessions[i], ",") {
session, err = findSessionCookieValue(possibleSessions[i], req.Cookies())
if err != nil {
return "", nil, http.StatusBadRequest, err
}
} else {
session = possibleSessions[i]
}
decoded, err = base64.StdEncoding.DecodeString(session)
if err != nil {
log.Warningf(ctx, "attempted to decode session but failed: %v", err)
continue
Expand Down
76 changes: 75 additions & 1 deletion pkg/server/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/cockroachdb/cockroach/pkg/base"
Expand All @@ -30,6 +31,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/settings"
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
"github.com/cockroachdb/cockroach/pkg/sql"
"github.com/cockroachdb/cockroach/pkg/sql/sem/catconstants"
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
"github.com/cockroachdb/cockroach/pkg/sql/sessiondata"
"github.com/cockroachdb/cockroach/pkg/sql/types"
Expand Down Expand Up @@ -627,7 +629,16 @@ func (am *authenticationMux) getSession(
continue
}
found = true
cookie, err = decodeSessionCookie(c)
sessionCookie := c
// This case is if the session cookie has a multi-tenant pattern.
if strings.Contains(c.Value, ",") {
sessionVal, err := findSessionCookieValue(c.Value, cookies)
if err != nil {
return "", nil, apiInternalError(req.Context(), err)
}
sessionCookie.Value = sessionVal
}
cookie, err = decodeSessionCookie(sessionCookie)
if err != nil {
// Multiple cookies with the same name may be included in the
// header. We continue searching even if we find a matching
Expand Down Expand Up @@ -696,3 +707,66 @@ func forwardAuthenticationMetadata(ctx context.Context, _ *http.Request) metadat
}
return md
}

// sessionCookieValue defines the data needed to construct the
// aggregate session cookie in the order provided.
type sessionCookieValue struct {
// The name of the tenant.
name string
// The value of set-cookie.
setCookie string
}

// createAggregatedSessionCookieValue is used for multi-tenant login.
// It takes a tenant name to set cookie map and converts it to a single
// string which is the aggregated session. Currently the format of the
// aggregated session is: session,tenant_name&session2,tenant_name2 etc.
func createAggregatedSessionCookieValue(sessionCookieValue []sessionCookieValue) string {
var sessionsStr string
for _, val := range sessionCookieValue {
sessionCookieSlice := strings.Split(strings.ReplaceAll(val.setCookie, "session=", ""), ";")
sessionsStr += sessionCookieSlice[0] + "," + val.name + "&"
}
if len(sessionsStr) > 0 {
sessionsStr = sessionsStr[:len(sessionsStr)-1]
}
return sessionsStr
}

// findSessionCookieValue finds the encoded session in an aggregated
// session cookie value established in multi-tenant clusters. If the
// method cannot find a match between the tenant name and session
// or cannot find the tenant cookie value, it will return an empty
// string to indicate this.
func findSessionCookieValue(sessionStr string, cookies []*http.Cookie) (string, error) {
tenantName := findTenantCookieValue(cookies)
if tenantName == "" {
return "", errors.New("unable to find tenant cookie")
}
sessionSlice := strings.FieldsFunc(sessionStr, func(r rune) bool {
return r == ',' || r == '&'
})
var encodedSession string
for idx, val := range sessionSlice {
if val == tenantName && idx > 0 {
encodedSession = sessionSlice[idx-1]
}
}
if encodedSession == "" {
return "", errors.Newf("unable to find session cookie value that matches tenant %q", tenantName)
}
return encodedSession, nil
}

// findTenantCookieValue iterates through all request cookies
// in order to find the value of the tenant cookie. If
// the tenant cookie is not found, it assumes the default
// to be the system tenant.
func findTenantCookieValue(cookies []*http.Cookie) string {
for _, c := range cookies {
if c.Name == TenantSelectCookieName {
return c.Value
}
}
return catconstants.SystemTenantName
}
68 changes: 68 additions & 0 deletions pkg/server/authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/cockroachdb/errors"
"github.com/gogo/protobuf/jsonpb"
"github.com/lib/pq"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
Expand Down Expand Up @@ -838,3 +839,70 @@ func TestGRPCAuthentication(t *testing.T) {
})
}
}

func TestCreateAggregatedSessionCookieValue(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)

tests := []struct {
name string
mapArg []sessionCookieValue
resExpected string
}{
{"standard arg", []sessionCookieValue{
{name: "system", setCookie: "session=abcd1234"},
{name: "app", setCookie: "session=efgh5678"}},
"abcd1234,system&efgh5678,app",
},
{"empty arg", []sessionCookieValue{}, ""},
}
for _, test := range tests {
t.Run(fmt.Sprintf("create-session-cookie/%s", test.name), func(t *testing.T) {
res := createAggregatedSessionCookieValue(test.mapArg)
require.Equal(t, test.resExpected, res)
})
}
}

func TestFindSessionCookieValue(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)
normalSessionStr := "abcd1234,system&efgh5678,app"
tests := []struct {
name string
sessionArg string
cookieArg []*http.Cookie
resExpected string
errorExpected bool
}{
{"standard args", normalSessionStr, []*http.Cookie{
{
Name: TenantSelectCookieName,
Value: "system",
Path: "/",
},
}, "abcd1234", false},
{"no tenant cookie", normalSessionStr, []*http.Cookie{}, "abcd1234", false},
{"invalid tenant cookie", normalSessionStr, []*http.Cookie{
{
Name: TenantSelectCookieName,
Value: "",
Path: "/",
},
}, "", true},
{"no tenant name match", normalSessionStr, []*http.Cookie{
{
Name: TenantSelectCookieName,
Value: "app2",
Path: "/",
},
}, "", true},
}
for _, test := range tests {
t.Run(fmt.Sprintf("find-session-cookie/%s", test.name), func(t *testing.T) {
res, err := findSessionCookieValue(test.sessionArg, test.cookieArg)
require.Equal(t, test.resExpected, res)
require.Equal(t, test.errorExpected, err != nil)
})
}
}
Loading

0 comments on commit cb7c30a

Please sign in to comment.