diff --git a/server/console.go b/server/console.go index ab3a48338..cd5752684 100644 --- a/server/console.go +++ b/server/console.go @@ -175,7 +175,7 @@ func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.D serverOpts := []grpc.ServerOption{ //grpc.StatsHandler(&ocgrpc.ServerHandler{IsPublicEndpoint: true}), grpc.MaxRecvMsgSize(int(config.GetConsole().MaxMessageSizeBytes)), - grpc.UnaryInterceptor(consoleInterceptorFunc(logger, config, consoleSessionCache)), + grpc.UnaryInterceptor(consoleInterceptorFunc(logger, config, consoleSessionCache, loginAttemptCache)), } grpcServer := grpc.NewServer(serverOpts...) @@ -438,7 +438,7 @@ func (s *ConsoleServer) Stop() { s.grpcServer.GracefulStop() } -func consoleInterceptorFunc(logger *zap.Logger, config Config, sessionCache SessionCache) func(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error) { +func consoleInterceptorFunc(logger *zap.Logger, config Config, sessionCache SessionCache, loginAttmeptCache LoginAttemptCache) func(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error) { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if info.FullMethod == "/nakama.console.Console/Authenticate" { // Skip authentication check for Login endpoint. @@ -464,7 +464,7 @@ func consoleInterceptorFunc(logger *zap.Logger, config Config, sessionCache Sess return nil, status.Error(codes.Unauthenticated, "Console authentication required.") } - if ctx, ok = checkAuth(ctx, config, auth[0], sessionCache); !ok { + if ctx, ok = checkAuth(ctx, logger, config, auth[0], sessionCache, loginAttmeptCache); !ok { return nil, status.Error(codes.Unauthenticated, "Console authentication invalid.") } role := ctx.Value(ctxConsoleRoleKey{}).(console.UserRole) @@ -478,7 +478,7 @@ func consoleInterceptorFunc(logger *zap.Logger, config Config, sessionCache Sess } } -func checkAuth(ctx context.Context, config Config, auth string, sessionCache SessionCache) (context.Context, bool) { +func checkAuth(ctx context.Context, logger *zap.Logger, config Config, auth string, sessionCache SessionCache, loginAttemptCache LoginAttemptCache) (context.Context, bool) { const basicPrefix = "Basic " const bearerPrefix = "Bearer " @@ -488,9 +488,24 @@ func checkAuth(ctx context.Context, config Config, auth string, sessionCache Ses if !ok { return ctx, false } - - if username != config.GetConsole().Username || password != config.GetConsole().Password { - // Username and/or password do not match. + ip, _ := extractClientAddressFromContext(logger, ctx) + if !loginAttemptCache.Allow(username, ip) { + return ctx, false + } + if username == config.GetConsole().Username { + if password != config.GetConsole().Password { + // Admin password does not match. + if lockout, until := loginAttemptCache.Add(config.GetConsole().Username, ip); lockout != LockoutTypeNone { + switch lockout { + case LockoutTypeAccount: + logger.Info(fmt.Sprintf("Console admin account locked until %v.", until)) + case LockoutTypeIp: + logger.Info(fmt.Sprintf("Console admin IP locked until %v.", until)) + } + } + return ctx, false + } + } else { return ctx, false } diff --git a/server/console_storage_import.go b/server/console_storage_import.go index 9a0366d92..c44aa4d74 100644 --- a/server/console_storage_import.go +++ b/server/console_storage_import.go @@ -54,7 +54,7 @@ func (s *ConsoleServer) importStorage(w http.ResponseWriter, r *http.Request) { } return } - ctx, ok := checkAuth(r.Context(), s.config, auth, s.consoleSessionCache) + ctx, ok := checkAuth(r.Context(), s.logger, s.config, auth, s.consoleSessionCache, s.loginAttemptCache) if !ok { w.WriteHeader(401) if _, err := w.Write([]byte("Console authentication invalid.")); err != nil { diff --git a/server/console_user.go b/server/console_user.go index e2e5a161b..1d1e0327b 100644 --- a/server/console_user.go +++ b/server/console_user.go @@ -17,6 +17,7 @@ package server import ( "bytes" "context" + "database/sql" "encoding/json" "errors" "github.com/jackc/pgconn" @@ -118,13 +119,14 @@ func (s *ConsoleServer) dbInsertConsoleUser(ctx context.Context, in *console.Add } func (s *ConsoleServer) DeleteUser(ctx context.Context, in *console.Username) (*emptypb.Empty, error) { - - if deleted, err := s.dbDeleteConsoleUser(ctx, in.Username); err != nil { + deleted, id, err := s.dbDeleteConsoleUser(ctx, in.Username) + if err != nil { s.logger.Error("failed to delete console user", zap.Error(err), zap.String("username", in.Username)) return nil, status.Error(codes.Internal, "Internal Server Error") } else if !deleted { return nil, status.Error(codes.InvalidArgument, "User not found") } + s.consoleSessionCache.RemoveAll(id) return &emptypb.Empty{}, nil } @@ -154,17 +156,15 @@ func (s *ConsoleServer) dbListConsoleUsers(ctx context.Context) ([]*console.User return result, nil } -func (s *ConsoleServer) dbDeleteConsoleUser(ctx context.Context, username string) (bool, error) { - res, err := s.db.ExecContext(ctx, "DELETE FROM console_user WHERE username = $1", username) - if err != nil { - return false, err - } - if n, err := res.RowsAffected(); err != nil { - return false, err - } else if n == 0 { - return false, nil +func (s *ConsoleServer) dbDeleteConsoleUser(ctx context.Context, username string) (bool, uuid.UUID, error) { + var deletedID uuid.UUID + if err := s.db.QueryRowContext(ctx, "DELETE FROM console_user WHERE username = $1 RETURNING id", username).Scan(&deletedID); err != nil { + if err == sql.ErrNoRows { + return false, uuid.Nil, nil + } + return false, uuid.Nil, err } - return true, nil + return true, deletedID, nil } func isValidPassword(pwd string) bool {