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
20 changes: 10 additions & 10 deletions build_stream/api/auth/jwt_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def _load_private_key(self) -> str:
raise JWTCreationError(
f"JWT private key not found: {self.config.private_key_path}"
) from None
except IOError as e:
except IOError:
logger.error("Failed to read JWT private key")
raise JWTCreationError("Failed to read JWT private key") from None
return self._private_key
Expand All @@ -157,7 +157,7 @@ def _load_public_key(self) -> str:
raise JWTValidationError(
f"JWT public key not found: {self.config.public_key_path}"
) from None
except IOError as e:
except IOError:
logger.error("Failed to read JWT public key")
raise JWTValidationError("Failed to read JWT public key") from None
return self._public_key
Expand Down Expand Up @@ -214,7 +214,7 @@ def create_access_token(
)
logger.info("Access token created for client: %s", client_id[:8] + "...")
return token, int(expires_delta.total_seconds())
except Exception as e:
except Exception:
logger.error("Failed to create access token")
raise JWTCreationError("Failed to create access token") from None

Expand Down Expand Up @@ -253,15 +253,15 @@ def validate_token(self, token: str) -> TokenData:
except ExpiredSignatureError:
logger.warning("Token has expired")
raise JWTExpiredError("Token has expired") from None
except (InvalidAudienceError, InvalidIssuerError) as e:
except (InvalidAudienceError, InvalidIssuerError):
logger.warning("Invalid token claims")
raise JWTValidationError("Invalid token claims") from None
except InvalidSignatureError:
logger.warning("Invalid token signature")
raise JWTInvalidSignatureError("Invalid token signature") from None
except DecodeError as e:
logger.warning("Invalid token format")
raise JWTValidationError("Invalid token format") from None
except Exception as e:
logger.error("Unexpected error validating token")
raise JWTValidationError("Token validation failed") from None
except DecodeError:
logger.warning("Invalid token format")
raise JWTValidationError("Invalid token format") from None
except Exception:
logger.error("Unexpected error validating token")
raise JWTValidationError("Token validation failed") from None
3 changes: 2 additions & 1 deletion build_stream/api/auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

from api.vault_client import VaultError

from .schemas import (
AuthErrorResponse,
ClientRegistrationRequest,
Expand All @@ -38,7 +40,6 @@
RegistrationDisabledError,
TokenCreationError,
)
from .vault_client import VaultError

logger = logging.getLogger(__name__)

Expand Down
61 changes: 25 additions & 36 deletions build_stream/api/auth/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Authentication service for OAuth2 client registration and token generation."""
"""Authentication service for OAuth2 client management."""

import logging
import os
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import List, Optional

from .jwt_handler import JWTCreationError, JWTHandler
from .password_handler import generate_credentials, verify_password
from .vault_client import VaultClient, VaultDecryptError, VaultError, VaultNotFoundError
from api.auth.jwt_handler import JWTHandler, JWTCreationError
from api.auth.password_handler import generate_credentials, verify_password
from api.logging_utils import log_secure_info
from api.vault_client import VaultClient, VaultDecryptError, VaultNotFoundError
from core.exceptions import (
ClientDisabledError,
InvalidClientError,
InvalidScopeError,
TokenCreationError,
)

logger = logging.getLogger(__name__)

Expand All @@ -45,22 +52,6 @@ class RegistrationDisabledError(Exception):
"""Exception raised when registration is disabled or misconfigured."""


class InvalidClientError(Exception):
"""Exception raised when client credentials are invalid."""


class ClientDisabledError(Exception):
"""Exception raised when client account is disabled."""


class InvalidScopeError(Exception):
"""Exception raised when requested scope is not allowed."""


class TokenCreationError(Exception):
"""Exception raised when token creation fails."""


@dataclass
class RegisteredClient:
"""Data class representing a registered OAuth client."""
Expand Down Expand Up @@ -187,10 +178,7 @@ def register_client(
"is_active": True,
}

try:
self.vault_client.save_oauth_client(client_id, client_data)
except VaultError:
raise
self.vault_client.save_oauth_client(client_id, client_data)

return RegisteredClient(
client_id=client_id,
Expand Down Expand Up @@ -222,25 +210,26 @@ def verify_client_credentials(
try:
oauth_clients = self.vault_client.get_oauth_clients()
except (VaultNotFoundError, VaultDecryptError):
logger.error("Failed to load OAuth clients from vault")
log_secure_info("error", "Failed to load OAuth clients from vault")
# Ensure no exception details are exposed
raise InvalidClientError("Client authentication failed") from None

if client_id not in oauth_clients:
logger.warning("Unknown client_id attempted: %s", client_id[:8] + "...")
log_secure_info("warning", "Unknown client_id attempted authentication", client_id)
raise InvalidClientError("Client authentication failed")

client_data = oauth_clients[client_id]

if not client_data.get("is_active", False):
logger.warning("Disabled client attempted token request: %s", client_id[:8] + "...")
log_secure_info("warning", "Disabled client attempted token request", client_id)
raise ClientDisabledError("Client account is disabled")

stored_hash = client_data.get("client_secret_hash")
if not stored_hash or not verify_password(client_secret, stored_hash):
logger.warning("Invalid client secret for: %s", client_id[:8] + "...")
log_secure_info("warning", "Invalid client secret provided", client_id)
raise InvalidClientError("Client authentication failed")

logger.info("Client credentials verified: %s", client_id[:8] + "...")
log_secure_info("info", "Client credentials verified successfully", client_id)
return client_data

def generate_token(
Expand Down Expand Up @@ -274,10 +263,10 @@ def generate_token(
requested_scopes = requested_scope.split()
for scope in requested_scopes:
if scope not in allowed_scopes:
logger.warning(
"Client %s requested unauthorized scope: %s",
client_id[:8] + "...",
scope,
log_secure_info(
"warning",
f"Client requested unauthorized scope: {scope}",
client_id
)
raise InvalidScopeError(f"Scope '{scope}' is not allowed for this client")
granted_scopes = requested_scopes
Expand All @@ -290,11 +279,11 @@ def generate_token(
client_name=client_name,
scopes=granted_scopes,
)
except JWTCreationError as e:
logger.error("Failed to create access token")
except JWTCreationError:
log_secure_info("error", "Failed to create access token", client_id)
raise TokenCreationError("Failed to create access token") from None

logger.info("Token generated for client: %s", client_id[:8] + "...")
log_secure_info("info", "Access token generated successfully", client_id)

return TokenResult(
access_token=access_token,
Expand Down
7 changes: 4 additions & 3 deletions build_stream/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
JWTInvalidSignatureError,
JWTValidationError,
)
from api.logging_utils import log_secure_info

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -73,7 +74,7 @@ def verify_token(

try:
token_data = jwt_handler.validate_token(credentials.credentials)
logger.info("Token validated successfully for client: %s", token_data.client_id[:8] + "...")
log_secure_info("info", "Token validated successfully", token_data.client_id)

return {
"client_id": token_data.client_id,
Expand Down Expand Up @@ -104,8 +105,8 @@ def verify_token(
headers={"WWW-Authenticate": "Bearer"},
) from None

except JWTValidationError as e:
logger.warning("Token validation failed: %s", str(e))
except JWTValidationError:
logger.warning("Token validation failed: Invalid token format or content")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
Expand Down
43 changes: 43 additions & 0 deletions build_stream/api/logging_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Generic secure logging utilities for Build Stream API."""

import logging
from typing import Optional


def log_secure_info(level: str, message: str, identifier: Optional[str] = None) -> None:
"""Log information securely with optional identifier truncation.

This function provides consistent secure logging across all modules.
When an identifier is provided, only the first 8 characters are logged
to prevent exposure of sensitive data while maintaining debugging capability.

Args:
level: Log level ('info', 'warning', 'error', 'debug', 'critical')
message: Log message template
identifier: Optional identifier (client_id, token_id, etc.) - first 8 chars logged
"""
logger = logging.getLogger(__name__)

if identifier:
# Always log first 8 characters for identification
log_message = f"{message}: {identifier[:8]}..."
else:
# Generic message when no identifier context
log_message = message

log_func = getattr(logger, level)
log_func(log_message)
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def _run_vault_command(
timeout=30,
)
return result.stdout
except subprocess.CalledProcessError as e:
except subprocess.CalledProcessError:
logger.error("Vault command failed: %s", command)
if command == "view":
raise VaultDecryptError("Failed to decrypt vault") from None
Expand All @@ -143,7 +143,7 @@ def read_vault(self, vault_path: str) -> Dict[str, Any]:
output = self._run_vault_command("view", vault_path)
try:
return yaml.safe_load(output) or {}
except yaml.YAMLError as e:
except yaml.YAMLError:
logger.error("Failed to parse vault YAML")
raise VaultDecryptError("Invalid vault content format") from None

Expand Down Expand Up @@ -187,7 +187,7 @@ def write_vault(self, vault_path: str, data: Dict[str, Any]) -> None:
"--encrypt-vault-id",
"default",
]
result = subprocess.run(
subprocess.run(
encrypt_cmd,
check=True,
capture_output=True,
Expand Down
31 changes: 31 additions & 0 deletions build_stream/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Core exceptions for the Build Stream API."""


class ClientDisabledError(Exception):
"""Exception raised when client account is disabled."""


class InvalidClientError(Exception):
"""Exception raised when client credentials are invalid."""


class InvalidScopeError(Exception):
"""Exception raised when requested scope is not allowed."""


class TokenCreationError(Exception):
"""Exception raised when token creation fails."""
Loading