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
4 changes: 3 additions & 1 deletion keepercommander/resources/service_config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ngrok_prompt = Enable Ngrok Tunneling? (y/n):
ngrok_token_prompt = Enter Ngrok Auth Token:
ngrok_custom_domain_prompt = Enter Ngrok Custom Domain:
run_mode_prompt = Select run mode (foreground/background):
queue_enabled_prompt = Enable Request Queue? (y/n):
tls_certificate = Enable TLS Certificate? (y/n):
certfile = Enter Certificate Path:
certpassword = Enter Certificate Password:
Expand All @@ -25,4 +26,5 @@ invalid_rate_limit = Invalid rate limit:
invalid_ip_list = Invalid IP list:
invalid_encryption_key = Invalid encryption key:
invalid_input = Invalid input. Please enter 'y' or 'n'.
invalid_format = Invalid format. Please enter either 'json' or 'yaml'.
invalid_format = Invalid format. Please enter either 'json' or 'yaml'.
invalid_queue_setting = Invalid queue setting. Please enter 'y' or 'n'.
31 changes: 24 additions & 7 deletions keepercommander/service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ You'll be prompted to configure:
- Enable TLS Certificate (y/n)
- TLS Certificate path
- TLS Certificate password
- Enable Request Queue (y/n)
- Advanced Security (y/n)
- Rate Limit
- Allowed IP List (comma-separated)
Expand All @@ -57,13 +58,13 @@ You'll be prompted to configure:
Configure the service streamlined with TLS:

```bash
My Vault> service-create -p <port> -f <json-or-yaml> -c 'tree,ls,search,record-add,mkdir' -rm <foreground-or-background> -crtf <certificate-file-path> -crtp <certificate-password-key-path> -aip <allowed-ip-list> -dip <denied-ip-list>
My Vault> service-create -p <port> -f <json-or-yaml> -c 'tree,ls,search,record-add,mkdir' -rm <foreground-or-background> -q <y-or-n> -crtf <certificate-file-path> -crtp <certificate-password-key-path> -aip <allowed-ip-list> -dip <denied-ip-list>
```

Configure the service streamlined with Ngrok:

```bash
My Vault> service-create -p <port> -f <json-or-yaml> -c 'tree,record-add,audit-report' -ng <ngrok-token> -cd <ngrok_custom_domain> -rm <foreground-or-background> -aip <allowed-ip-list> -dip <denied-ip-list>
My Vault> service-create -p <port> -f <json-or-yaml> -c 'tree,record-add,audit-report' -ng <ngrok-token> -cd <ngrok_custom_domain> -rm <foreground-or-background> -q <y-or-n> -aip <allowed-ip-list> -dip <denied-ip-list>
```

Parameters:
Expand All @@ -75,6 +76,7 @@ Parameters:
- `-crtf, --certfile`: Certificate file path
- `-crtp, --certpassword`: Certificate password
- `-rm, --run_mode`: Run mode (foreground/background)
- `-q, --queue_enabled`: Enable request queue (y/n)
- `-dip, --deniedip`: Denied IP list to access service
- `-aip, --allowedip`: Allowed IP list to access service

Expand All @@ -97,6 +99,12 @@ My Vault> service-stop

## API Usage

### API Versioning

The service provides two API versions based on queue configuration:
- **`/api/v2/`** - Queue enabled (default): Asynchronous request processing with enhanced features
- **`/api/v1/`** - Queue disabled (legacy): Direct synchronous execution

### Request Queue System

The service uses an asynchronous request queue system that provides:
Expand All @@ -109,7 +117,7 @@ The service uses an asynchronous request queue system that provides:

**Submit Request:**
```bash
curl -X POST 'http://localhost:<port>/api/v1/executecommand' \
curl -X POST 'http://localhost:<port>/api/v2/executecommand-async' \
--header 'Content-Type: application/json' \
--header 'api-key: <your-api-key>' \
--data '{"command": "tree"}'
Expand All @@ -120,13 +128,13 @@ curl -X POST 'http://localhost:<port>/api/v1/executecommand' \
"success": true,
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"message": "Request queued successfully. Use /api/v1/status/<request_id> to check progress, /api/v1/result/<request_id> to get results, or /api/v1/queue/status for queue info."
"message": "Request queued successfully. Use /api/v2/status/<request_id> to check progress, /api/v2/result/<request_id> to get results, or /api/v2/queue/status for queue info."
}
```

**Check Request Status:**
```bash
curl 'http://localhost:<port>/api/v1/status/<request_id>' \
curl 'http://localhost:<port>/api/v2/status/<request_id>' \
--header 'api-key: <your-api-key>'
```
*Response:*
Expand All @@ -144,7 +152,7 @@ curl 'http://localhost:<port>/api/v1/status/<request_id>' \

**Get Request Result:**
```bash
curl 'http://localhost:<port>/api/v1/result/<request_id>' \
curl 'http://localhost:<port>/api/v2/result/<request_id>' \
--header 'api-key: <your-api-key>'
```
*Response (for completed request):*
Expand All @@ -157,7 +165,7 @@ curl 'http://localhost:<port>/api/v1/result/<request_id>' \

**Get Queue Status:**
```bash
curl 'http://localhost:<port>/api/v1/queue/status' \
curl 'http://localhost:<port>/api/v2/queue/status' \
--header 'api-key: <your-api-key>'
```
*Response:*
Expand Down Expand Up @@ -311,6 +319,15 @@ The service includes robust error handling for:
### Execute Command Endpoint

```bash
# Queue enabled (v2 - async)
curl --location 'http://localhost:<port>/api/v2/executecommand-async' \
--header 'Content-Type: application/json' \
--header 'api-key: <your-api-key>' \
--data '{
"command": "<command>"
}'

# Queue disabled (v1 - direct)
curl --location 'http://localhost:<port>/api/v1/executecommand' \
--header 'Content-Type: application/json' \
--header 'api-key: <your-api-key>' \
Expand Down
70 changes: 58 additions & 12 deletions keepercommander/service/api/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,73 @@
# Contact: ops@keepersecurity.com
#

from flask import Blueprint, request, jsonify
from flask import Blueprint, request, jsonify, Response
from html import escape
import queue
from typing import Tuple, Union
from ..decorators.unified import unified_api_decorator
from ..util.command_util import CommandExecutor
from ..decorators.logging import logger
from ..core.request_queue import queue_manager
from ..util.request_validation import RequestValidator

def create_legacy_command_blueprint():
"""Create legacy blueprint for direct/synchronous command execution (non-queue mode)."""
bp = Blueprint("legacy_command_bp", __name__)

@bp.after_request
def add_legacy_header(response):
"""Add legacy header for legacy API."""
response.headers['X-API-Legacy'] = 'true'
return response

@bp.route("/executecommand", methods=["POST"])
@unified_api_decorator()
def execute_command_direct(**kwargs) -> Tuple[Union[Response, bytes], int]:
"""Execute command directly and return result immediately (legacy behavior)."""
try:
logger.warning("LEGACY: /api/v1/ usage - migrate to /api/v2/")

json_error = RequestValidator.validate_request_json()
if json_error:
return json_error

command, validation_error = RequestValidator.validate_and_escape_command(request.json)
if validation_error:
return validation_error

response, status_code = CommandExecutor.execute(command)

# If we get a busy response, add v1-specific message
if (isinstance(response, dict) and
"temporarily busy" in str(response.get("error", "")).lower()):
response["message"] = "Note: api/v1/executecommand only supports a single request at a time."
status_code = 503

return response if isinstance(response, bytes) else jsonify(response), status_code

except Exception as e:
logger.error(f"Error executing command: {e}")
return jsonify({"success": False, "error": f"Error: {str(e)}"}), 500

return bp

def create_command_blueprint():
"""Create Blue Print for Keeper Commander Service."""
bp = Blueprint("command_bp", __name__)

@bp.route("/executecommand", methods=["POST"])
@bp.route("/executecommand-async", methods=["POST"])
@unified_api_decorator()
def execute_command(**kwargs):
def execute_command(**kwargs) -> Tuple[Response, int]:
"""Submit a command for execution and return request ID immediately."""
try:
request_command = request.json.get("command")
if not request_command:
return jsonify({"success": False, "error": "Error: No command provided"}), 400

command = escape(request_command)
json_error = RequestValidator.validate_request_json()
if json_error:
return json_error

command, validation_error = RequestValidator.validate_and_escape_command(request.json)
if validation_error:
return validation_error

# Submit to queue and return request ID immediately
try:
Expand All @@ -38,7 +84,7 @@ def execute_command(**kwargs):
"success": True,
"request_id": request_id,
"status": "queued",
"message": "Request queued successfully. Use /api/v1/status/<request_id> to check progress, /api/v1/result/<request_id> to get results, or /api/v1/queue/status for queue info."
"message": "Request queued successfully. Use /api/v2/status/<request_id> to check progress, /api/v2/result/<request_id> to get results, or /api/v2/queue/status for queue info."
}), 202 # 202 Accepted
except queue.Full:
return jsonify({
Expand All @@ -52,7 +98,7 @@ def execute_command(**kwargs):

@bp.route("/status/<request_id>", methods=["GET"])
@unified_api_decorator()
def get_request_status(request_id, **kwargs):
def get_request_status(request_id: str, **kwargs) -> Tuple[Response, int]:
"""Get the status of a specific request."""
try:
status_info = queue_manager.get_request_status(request_id)
Expand All @@ -74,7 +120,7 @@ def get_request_status(request_id, **kwargs):

@bp.route("/result/<request_id>", methods=["GET"])
@unified_api_decorator()
def get_request_result(request_id, **kwargs):
def get_request_result(request_id: str, **kwargs) -> Tuple[Union[Response, bytes], int]:
"""Get the result of a completed request."""
try:
result_data = queue_manager.get_request_result(request_id)
Expand Down Expand Up @@ -102,7 +148,7 @@ def get_request_result(request_id, **kwargs):

@bp.route("/queue/status", methods=["GET"])
@unified_api_decorator()
def get_queue_status(**kwargs):
def get_queue_status(**kwargs) -> Tuple[Response, int]:
"""Get overall queue status information."""
try:
queue_status = queue_manager.get_queue_status()
Expand Down
40 changes: 34 additions & 6 deletions keepercommander/service/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,47 @@

from flask import Flask
from typing import Optional
from .command import create_command_blueprint
from .command import create_command_blueprint, create_legacy_command_blueprint
from ..decorators.logging import logger, debug_decorator

def _setup_queue_mode(app: Flask) -> None:
"""Setup queue mode with v2 API endpoints."""
from ..core.request_queue import queue_manager
queue_manager.start()

command_bp = create_command_blueprint()
app.register_blueprint(command_bp, url_prefix='/api/v2')
logger.debug("Started queue manager and registered command blueprint with URL prefix '/api/v2'")

def _setup_legacy_mode(app: Flask) -> None:
"""Setup legacy mode with v1 API endpoints."""
legacy_bp = create_legacy_command_blueprint()
app.register_blueprint(legacy_bp, url_prefix='/api/v1')
logger.info("Using LEGACY /api/v1 - Enable queue mode (-q y) for /api/v2")

@debug_decorator
def init_routes(app: Optional[Flask] = None) -> None:
"""Initialize routes for the Keeper Commander Service."""
"""Initialize routes and queue manager for the Keeper Commander Service."""
if app is None:
raise ValueError("App instance is required")

logger.debug("Starting route initialization")
command_bp = create_command_blueprint()

logger.debug("Registering command blueprint with URL prefix '/api/v1'")
app.register_blueprint(command_bp, url_prefix='/api/v1')

try:
from ..config.service_config import ServiceConfig
service_config = ServiceConfig()
config_data = service_config.load_config()
queue_enabled = config_data.get("queue_enabled", "y") # Default to enabled

if queue_enabled == "y":
logger.debug("Queue enabled - setting up v2 API with request queue")
_setup_queue_mode(app)
else:
logger.debug("Queue disabled - setting up v1 API with direct execution")
_setup_legacy_mode(app)

except Exception as e:
logger.warning(f"Could not load service config, defaulting to queue mode: {e}")
_setup_queue_mode(app)

logger.debug("Route initialization completed successfully")
4 changes: 0 additions & 4 deletions keepercommander/service/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from .decorators.security import limiter
from .api.routes import init_routes
from .decorators.logging import logger
from .core.request_queue import queue_manager


def create_app():
Expand All @@ -31,9 +30,6 @@ def create_app():
try:
logger.debug("Configuring rate limiter")
limiter.init_app(app)

logger.debug("Starting request queue manager")
queue_manager.start()

logger.debug("Initializing API routes")
init_routes(app)
Expand Down
4 changes: 3 additions & 1 deletion keepercommander/service/commands/create_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class StreamlineArgs:
certpassword : Optional[str]
fileformat : Optional[str]
run_mode: Optional[str]
queue_enabled: Optional[str]

class CreateService(Command):
"""Command to create a new service configuration."""
Expand Down Expand Up @@ -67,6 +68,7 @@ def get_parser(self):
parser.add_argument('-crtp', '--certpassword', type=str, help='certificate password')
parser.add_argument('-f', '--fileformat', type=str, help='file format')
parser.add_argument('-rm', '--run_mode', type=str, help='run mode')
parser.add_argument('-q', '--queue_enabled', type=str, help='enable request queue (y/n)')
return parser

def execute(self, params: KeeperParams, **kwargs) -> None:
Expand All @@ -81,7 +83,7 @@ def execute(self, params: KeeperParams, **kwargs) -> None:

config_data = self.service_config.create_default_config()

filtered_kwargs = {k: v for k, v in kwargs.items() if k in ['port', 'allowedip', 'deniedip', 'commands', 'ngrok', 'ngrok_custom_domain', 'certfile', 'certpassword', 'fileformat', 'run_mode']}
filtered_kwargs = {k: v for k, v in kwargs.items() if k in ['port', 'allowedip', 'deniedip', 'commands', 'ngrok', 'ngrok_custom_domain', 'certfile', 'certpassword', 'fileformat', 'run_mode', 'queue_enabled']}
args = StreamlineArgs(**filtered_kwargs)
self._handle_configuration(config_data, params, args)
self._create_and_save_record(config_data, params, args)
Expand Down
13 changes: 12 additions & 1 deletion keepercommander/service/commands/service_config_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def handle_streamlined_config(self, config_data: Dict[str, Any], args, params: K
if args.fileformat is not None and args.fileformat not in ['json', 'yaml']:
raise ValidationError(f"Invalid file format '{args.fileformat}'. Must be 'json' or 'yaml'.")

queue_enabled = args.queue_enabled if args.queue_enabled is not None else "y"
if args.queue_enabled is not None and queue_enabled not in ['y', 'n']:
raise ValidationError(f"Invalid queue setting '{queue_enabled}'. Must be 'y' or 'n'.")

config_data.update({
"port": self.service_config.validator.validate_port(args.port),
"ip_allowed_list": self.service_config.validator.validate_ip_list(args.allowedip),
Expand All @@ -52,14 +56,16 @@ def handle_streamlined_config(self, config_data: Dict[str, Any], args, params: K
"certfile": args.certfile,
"certpassword": args.certpassword,
"fileformat": args.fileformat, # Keep original logic - can be None
"run_mode": run_mode
"run_mode": run_mode,
"queue_enabled": queue_enabled
})

@debug_decorator
def handle_interactive_config(self, config_data: Dict[str, Any], params: KeeperParams) -> None:
self._configure_port(config_data)
self._configure_ngrok(config_data)
self._configure_tls(config_data)
self._configure_queue(config_data)

config_data["fileformat"] = None

Expand Down Expand Up @@ -108,6 +114,11 @@ def _configure_tls(self, config_data: Dict[str, Any]) -> None:
config_data["certfile"] = ""
config_data["certpassword"] = ""

def _configure_queue(self, config_data: Dict[str, Any]) -> None:
"""Configure queue enabled setting with user prompt."""
config_data["queue_enabled"] = self.service_config._get_yes_no_input(self.messages['queue_enabled_prompt'])
logger.debug(f"Queue enabled set to: {config_data['queue_enabled']}")

def _configure_run_mode(self, config_data: Dict[str, Any]) -> None:
"""Configure run mode with user prompt."""
while True:
Expand Down
1 change: 1 addition & 0 deletions keepercommander/service/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ class ServiceConfigData:
encryption_private_key: str
fileformat: str
run_mode: str
queue_enabled: str
records: List[Dict[str, Any]]
Loading