Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor auth feature flag #1089

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
76 changes: 20 additions & 56 deletions pkg/apiserver/user/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,63 +18,22 @@ import (
"github.com/pingcap/log"
"go.uber.org/zap"

"github.com/pingcap/tidb-dashboard/pkg/apiserver/user/shared"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap/tidb-dashboard/util/featureflag"
"github.com/pingcap/tidb-dashboard/util/rest"
)

var (
ErrNS = errorx.NewNamespace("error.api.user")
ErrUnsupportedAuthType = ErrNS.NewType("unsupported_auth_type")
ErrNSSignIn = ErrNS.NewSubNamespace("signin")
ErrSignInOther = ErrNSSignIn.NewType("other")
)

type AuthService struct {
FeatureFlagNonRootLogin *featureflag.FeatureFlag

middleware *jwt.GinJWTMiddleware
authenticators map[utils.AuthType]Authenticator
}

type AuthenticateForm struct {
Type utils.AuthType `json:"type" example:"0"`
Username string `json:"username" example:"root"` // Does not present for AuthTypeSharingCode
Password string `json:"password"`
Extra string `json:"extra"` // FIXME: Use strong type
authenticators map[utils.AuthType]shared.Authenticator
}

type TokenResponse struct {
Token string `json:"token"`
Expire time.Time `json:"expire"`
}

type SignOutInfo struct {
EndSessionURL string `json:"end_session_url"`
}

type Authenticator interface {
IsEnabled() (bool, error)
Authenticate(form AuthenticateForm) (*utils.SessionUser, error)
ProcessSession(u *utils.SessionUser) bool
SignOutInfo(u *utils.SessionUser, redirectURL string) (*SignOutInfo, error)
}

type BaseAuthenticator struct{}

func (a BaseAuthenticator) IsEnabled() (bool, error) {
return true, nil
}

func (a BaseAuthenticator) ProcessSession(u *utils.SessionUser) bool {
return true
}

func (a BaseAuthenticator) SignOutInfo(u *utils.SessionUser, redirectURL string) (*SignOutInfo, error) {
return &SignOutInfo{}, nil
}

func newAuthService(featureFlags *featureflag.Registry) *AuthService {
func newAuthService() *AuthService {
var secret *[32]byte

secretStr := os.Getenv("DASHBOARD_SESSION_SECRET")
Expand All @@ -91,9 +50,8 @@ func newAuthService(featureFlags *featureflag.Registry) *AuthService {
}

service := &AuthService{
FeatureFlagNonRootLogin: featureFlags.Register("nonRootLogin", ">= 5.3.0"),
middleware: nil,
authenticators: map[utils.AuthType]Authenticator{},
middleware: nil,
authenticators: map[utils.AuthType]shared.Authenticator{},
}

middleware, err := jwt.New(&jwt.GinJWTMiddleware{
Expand All @@ -103,7 +61,7 @@ func newAuthService(featureFlags *featureflag.Registry) *AuthService {
Timeout: time.Hour * 24,
MaxRefresh: time.Hour * 24,
Authenticator: func(c *gin.Context) (interface{}, error) {
var form AuthenticateForm
var form shared.AuthenticateForm
if err := c.ShouldBindJSON(&form); err != nil {
return nil, rest.ErrBadRequest.WrapWithNoMessage(err)
}
Expand Down Expand Up @@ -182,7 +140,7 @@ func newAuthService(featureFlags *featureflag.Registry) *AuthService {
err = e
} else if errors.Is(e, jwt.ErrFailedTokenCreation) {
// Try to catch other sign in failure errors.
err = ErrSignInOther.WrapWithNoMessage(e)
err = shared.ErrSignInOther.WrapWithNoMessage(e)
} else {
// The remaining error comes from checking tokens for protected endpoints.
err = rest.ErrUnauthenticated.NewWithNoMessage()
Expand Down Expand Up @@ -210,10 +168,10 @@ func newAuthService(featureFlags *featureflag.Registry) *AuthService {
return service
}

func (s *AuthService) authForm(f AuthenticateForm) (*utils.SessionUser, error) {
func (s *AuthService) authForm(f shared.AuthenticateForm) (*utils.SessionUser, error) {
a, ok := s.authenticators[f.Type]
if !ok {
return nil, ErrUnsupportedAuthType.NewWithNoMessage()
return nil, shared.ErrUnsupportedAuthType.NewWithNoMessage()
}
u, err := a.Authenticate(f)
if err != nil {
Expand Down Expand Up @@ -272,11 +230,17 @@ func (s *AuthService) MWRequireWritePriv() gin.HandlerFunc {
}
}

// RegisterAuthenticator registers an authenticator in the authenticate pipeline.
func (s *AuthService) RegisterAuthenticator(typeID utils.AuthType, a Authenticator) {
var _ shared.AuthenticatorRegister = (*AuthService)(nil)

// Register impl shared.AuthenticatorRegister, registers an authenticator in the authenticate pipeline.
func (s *AuthService) Register(typeID utils.AuthType, a shared.Authenticator) {
s.authenticators[typeID] = a
}

func provideAuthenticatorRegister(authService *AuthService) shared.AuthenticatorRegister {
return authService
}

type GetLoginInfoResponse struct {
SupportedAuthTypes []int `json:"supported_auth_types"`
}
Expand Down Expand Up @@ -306,7 +270,7 @@ func (s *AuthService) getLoginInfoHandler(c *gin.Context) {

// @ID userLogin
// @Summary Log in
// @Param message body AuthenticateForm true "Credentials"
// @Param message body shared.AuthenticateForm true "Credentials"
// @Success 200 {object} TokenResponse
// @Failure 401 {object} rest.ErrorResponse
// @Router /user/login [post]
Expand All @@ -320,7 +284,7 @@ type GetSignOutInfoRequest struct {

// @ID userGetSignOutInfo
// @Summary Get sign out info
// @Success 200 {object} SignOutInfo
// @Success 200 {object} shared.SignOutInfo
// @Param q query GetSignOutInfoRequest true "Query"
// @Router /user/sign_out_info [get]
// @Security JwtAuth
Expand All @@ -336,7 +300,7 @@ func (s *AuthService) getSignOutInfoHandler(c *gin.Context) {
u := utils.GetSession(c)
a, ok := s.authenticators[u.AuthFrom]
if !ok {
_ = c.Error(ErrUnsupportedAuthType.NewWithNoMessage())
_ = c.Error(shared.ErrUnsupportedAuthType.NewWithNoMessage())
return
}
si, err := a.SignOutInfo(u, req.RedirectURL)
Expand Down
19 changes: 7 additions & 12 deletions pkg/apiserver/user/code/codeauth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,31 @@ import (

"go.uber.org/fx"

"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user/code"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user/shared"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
)

const typeID utils.AuthType = 1

var ErrSignInInvalidCode = user.ErrNSSignIn.NewType("invalid_code") // Invalid or expired
var ErrSignInInvalidCode = shared.ErrNSSignIn.NewType("invalid_code") // Invalid or expired

type Authenticator struct {
user.BaseAuthenticator
shared.BaseAuthenticator
sharingCodeService *code.Service
}

func newAuthenticator(sharingCodeService *code.Service) *Authenticator {
return &Authenticator{
func registerAuthenticator(r shared.AuthenticatorRegister, sharingCodeService *code.Service) {
r.Register(typeID, &Authenticator{
sharingCodeService: sharingCodeService,
}
}

func registerAuthenticator(a *Authenticator, authService *user.AuthService) {
authService.RegisterAuthenticator(typeID, a)
})
}

var Module = fx.Options(
fx.Provide(newAuthenticator),
fx.Invoke(registerAuthenticator),
)

func (a *Authenticator) Authenticate(f user.AuthenticateForm) (*utils.SessionUser, error) {
func (a *Authenticator) Authenticate(f shared.AuthenticateForm) (*utils.SessionUser, error) {
session := a.sharingCodeService.NewSessionFromSharingCode(f.Password)
if session == nil {
return nil, ErrSignInInvalidCode.NewWithNoMessage()
Expand Down
8 changes: 7 additions & 1 deletion pkg/apiserver/user/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ package user

import (
"go.uber.org/fx"

"github.com/pingcap/tidb-dashboard/pkg/apiserver/user/shared"
)

var Module = fx.Options(
fx.Provide(newAuthService),
fx.Provide(
newAuthService,
provideAuthenticatorRegister,
shared.ProvideFeatureFlags,
),
fx.Invoke(registerRouter),
)
41 changes: 41 additions & 0 deletions pkg/apiserver/user/shared/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0.

package shared

import "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"

type AuthenticatorRegister interface {
Register(typeID utils.AuthType, a Authenticator)
}

type AuthenticateForm struct {
Type utils.AuthType `json:"type" example:"0"`
Username string `json:"username" example:"root"` // Does not present for AuthTypeSharingCode
Password string `json:"password"`
Extra string `json:"extra"` // FIXME: Use strong type
}

type SignOutInfo struct {
EndSessionURL string `json:"end_session_url"`
}

type Authenticator interface {
IsEnabled() (bool, error)
Authenticate(form AuthenticateForm) (*utils.SessionUser, error)
ProcessSession(u *utils.SessionUser) bool
SignOutInfo(u *utils.SessionUser, redirectURL string) (*SignOutInfo, error)
}

type BaseAuthenticator struct{}

func (a BaseAuthenticator) IsEnabled() (bool, error) {
return true, nil
}

func (a BaseAuthenticator) ProcessSession(u *utils.SessionUser) bool {
return true
}

func (a BaseAuthenticator) SignOutInfo(u *utils.SessionUser, redirectURL string) (*SignOutInfo, error) {
return &SignOutInfo{}, nil
}
14 changes: 14 additions & 0 deletions pkg/apiserver/user/shared/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0.

package shared

import "github.com/joomcode/errorx"

var (
ErrNS = errorx.NewNamespace("error.api.user")
ErrUnsupportedAuthType = ErrNS.NewType("unsupported_auth_type")
ErrUnsupportedUser = ErrNS.NewType("unsupported_user")
ErrNSSignIn = ErrNS.NewSubNamespace("signin")
ErrSignInOther = ErrNSSignIn.NewType("other")
ErrInsufficientPrivs = ErrNSSignIn.NewType("insufficient_priv")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is SQL sign-in specific and should not be put in shared

)
15 changes: 15 additions & 0 deletions pkg/apiserver/user/shared/feature_flag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0.

package shared

import "github.com/pingcap/tidb-dashboard/util/featureflag"

type UserFeatureFlags struct {
NonRootLogin *featureflag.FeatureFlag
Copy link
Member

@breezewish breezewish Dec 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is something shared. It is SQL sign-in specific thing. I would rather see SSO to rely on something of SQL sign-in, instead of introducing SQL sign-in for all authenticators via shared.

}

func ProvideFeatureFlags(featureFlags *featureflag.Registry) *UserFeatureFlags {
return &UserFeatureFlags{
NonRootLogin: featureFlags.Register("nonRootLogin", ">= 5.3.0"),
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0.

package user
package shared

import (
"encoding/json"
Expand All @@ -11,8 +11,6 @@ import (
"github.com/pingcap/tidb-dashboard/pkg/tidb"
)

var ErrInsufficientPrivs = ErrNSSignIn.NewType("insufficient_priv")

// TiDB config response
//
// "security": {
Expand All @@ -29,7 +27,11 @@ type tidbSEMConfig struct {
SkipGrantTable bool `json:"skip-grant-table"`
}

func VerifySQLUser(tidbClient *tidb.Client, userName, password string) (writeable bool, err error) {
func VerifySQLUser(tidbClient *tidb.Client, featureFlags *UserFeatureFlags, userName, password string) (writeable bool, err error) {
if !featureFlags.NonRootLogin.IsSupported() && userName != "root" {
return false, ErrUnsupportedUser.New("User must be root")
}

db, err := tidbClient.OpenSQLConn(userName, password)
if err != nil {
return false, err
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0.

package user
package shared

import (
"testing"
Expand Down
29 changes: 13 additions & 16 deletions pkg/apiserver/user/sqlauth/sqlauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,42 @@ import (
"github.com/joomcode/errorx"
"go.uber.org/fx"

"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user/shared"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap/tidb-dashboard/pkg/tidb"
)

const typeID utils.AuthType = 0

type Authenticator struct {
user.BaseAuthenticator
tidbClient *tidb.Client
shared.BaseAuthenticator
tidbClient *tidb.Client
userFeatureFlags *shared.UserFeatureFlags
}

func newAuthenticator(tidbClient *tidb.Client) *Authenticator {
return &Authenticator{
tidbClient: tidbClient,
}
}

func registerAuthenticator(a *Authenticator, authService *user.AuthService) {
authService.RegisterAuthenticator(typeID, a)
func registerAuthenticator(r shared.AuthenticatorRegister, tidbClient *tidb.Client, ff *shared.UserFeatureFlags) {
r.Register(typeID, &Authenticator{
tidbClient: tidbClient,
userFeatureFlags: ff,
})
}

var Module = fx.Options(
fx.Provide(newAuthenticator),
fx.Invoke(registerAuthenticator),
)

func (a *Authenticator) Authenticate(f user.AuthenticateForm) (*utils.SessionUser, error) {
writeable, err := user.VerifySQLUser(a.tidbClient, f.Username, f.Password)
func (a *Authenticator) Authenticate(f shared.AuthenticateForm) (*utils.SessionUser, error) {
writeable, err := shared.VerifySQLUser(a.tidbClient, a.userFeatureFlags, f.Username, f.Password)
if err != nil {
if errorx.Cast(err) == nil {
return nil, user.ErrSignInOther.WrapWithNoMessage(err)
return nil, shared.ErrSignInOther.WrapWithNoMessage(err)
}
// Possible errors could be:
// tidb.ErrNoAliveTiDB
// tidb.ErrPDAccessFailed
// tidb.ErrTiDBConnFailed
// tidb.ErrTiDBAuthFailed
// user.ErrInsufficientPrivs
// shared.ErrInsufficientPrivs
return nil, err
}

Expand Down
Loading