Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b26ed18
fix(secrets): Move changes from old api repo to monorepo branch
Nareshp1 Jul 20, 2025
7a17d57
fix(secrets): Simplified error handling for invalid credential payloads
Nareshp1 Jul 20, 2025
5663551
fix(tests): Ignore test coverage files
Nareshp1 Jul 20, 2025
2467078
feat(tests): Adding initial tests for creds verification
Nareshp1 Jul 20, 2025
10d6117
fix(secrets): Added additional handling for invalid provider in secre…
Nareshp1 Jul 20, 2025
9218f89
fix(tests): Updated AWS Secrets payload in config to reflect current …
Nareshp1 Jul 20, 2025
8517b8c
feat(tests): Added tests for CredsFactory class
Nareshp1 Jul 20, 2025
4b2ff5e
fix(schemas): Extra period in error message
Nareshp1 Jul 20, 2025
f5311d9
fix(tests): Update credential payload to use empty dictionary when te…
Nareshp1 Jul 20, 2025
e6bf1fc
fix(tests): fix api route for fixture test (in-progress)
Nareshp1 Jul 20, 2025
b8c3911
feat(tests): add test coverage for AWS secrets schema
Nareshp1 Jul 20, 2025
fcd20a2
fix(tests): Update pyproject.toml to ignore botocore deprecated warni…
Nareshp1 Jul 25, 2025
0ae48eb
fix(api): Update user error message for updating secrets endpoint.
Nareshp1 Jul 25, 2025
0b22652
fix(crud): Remove unnecessary commit line from crud function.
Nareshp1 Jul 25, 2025
83e76f8
fix(tests): Working full coverage unit api tests for users secrets en…
Nareshp1 Jul 25, 2025
b672026
feat(tests): working full unit test coverage for crud secrets.
Nareshp1 Jul 25, 2025
67c999f
feat(tests): full test coverage for creds verification on AWS.
Nareshp1 Jul 29, 2025
9603e5a
feat(frontend): Combined secrets pages into a single section with tab…
Nareshp1 Jul 29, 2025
61330c2
feat(frontend): Working creds verification endpoint from frontend.
Nareshp1 Jul 30, 2025
64dd799
feat(frontend): Updated function logic for creating the payload used …
Nareshp1 Jul 30, 2025
121c388
feat(cli): Updated secrets verification endpoint for cli
Nareshp1 Jul 30, 2025
cf3b147
fix(workflows): Fix ruff errors.
Nareshp1 Jul 30, 2025
d8ed96d
fix(api): add init for api cloud module
alexchristy Jul 31, 2025
c598a96
feat(tests): add integration tests for aws creds verification class
alexchristy Jul 31, 2025
ed0262b
fix(tests): fix adding credentials in deploy tests to match new secre…
alexchristy Jul 31, 2025
fd12763
Merge branch 'main' into secrets-verification
Nareshp1 Aug 1, 2025
9076142
fix(tests): Fix failing ruff tests
Nareshp1 Aug 1, 2025
332c895
fix(tests): Fix MyPy error with crud_secrets test.
Nareshp1 Aug 1, 2025
8982a19
fix(tests): More ruff error fixes :(
Nareshp1 Aug 1, 2025
3b15018
fix(tests): More ruff fixes :(
Nareshp1 Aug 1, 2025
4d7b3df
fix(tests): Fix ruff checks
Nareshp1 Aug 1, 2025
6109ada
Fix(secrets verification): Fix typos.
Nareshp1 Aug 1, 2025
be0a584
fix(tests): Updated test config for aws secrets and renamed aws tests…
Nareshp1 Aug 3, 2025
168f099
fix(secrets verification): Update secrets functionality to use discri…
Nareshp1 Aug 3, 2025
0d3dc84
fix(secrets verification): Update endpoint to use newly created secre…
Nareshp1 Aug 3, 2025
9c68d2a
fix(secrets verification): Update provider enum for discriminated uni…
Nareshp1 Aug 3, 2025
08a52cf
fix(tests): Update secrets tests to use new discriminated union logic.
Nareshp1 Aug 3, 2025
ac16d37
fix(tests): Fix usage of old CredsVerify Schema to use new discrimina…
Nareshp1 Aug 3, 2025
8c9136f
fix(secrets verification): Ruff fixes.
Nareshp1 Aug 3, 2025
22551dd
fix:(cli): Updated cli to use updated credential payload.
Nareshp1 Aug 3, 2025
89a90d9
fix(frontend): Updated endpoint calls to use new credential payload.
Nareshp1 Aug 3, 2025
6872182
fix(schemas): Updated error message for aws secrets validators.
Nareshp1 Aug 6, 2025
d6adab5
feat(frontend): Added additional parsing for pydantic validation erro…
Nareshp1 Aug 6, 2025
cc8d069
spacing fix.
Nareshp1 Aug 6, 2025
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ testing-out/
# Misc
.vscode/

# Tests
.coverage
3 changes: 2 additions & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ addopts = [
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
filterwarnings = [
"ignore:cannot collect test class 'Testing':pytest.PytestCollectionWarning"
"ignore:cannot collect test class 'Testing':pytest.PytestCollectionWarning",
"ignore::DeprecationWarning:botocore.auth",
]

# Register markers to easily select unit or integration tests.
Expand Down
5 changes: 4 additions & 1 deletion api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ argon2-cffi>=23.1.0
# Async Jobs
arq>=0.26.3
aiofiles>=24.1.0
tenacity>=9.1.2
tenacity>=9.1.2

# Provider SDKs
boto3>=1.38.0
122 changes: 37 additions & 85 deletions api/src/app/api/v1/users.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import base64
from datetime import UTC, datetime

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import JSONResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio.session import AsyncSession

from src.app.cloud.creds_factory import CredsFactory

from ...core.auth.auth import get_current_user
from ...core.config import settings
from ...core.db.database import async_get_db
from ...crud.crud_secrets import get_user_secrets, upsert_user_secrets
from ...crud.crud_users import get_user_by_id, update_user_password
from ...models.secret_model import SecretModel
from ...models.user_model import UserModel
from ...schemas.message_schema import (
AWSUpdateSecretMessageSchema,
AzureUpdateSecretMessageSchema,
MessageSchema,
UpdatePasswordMessageSchema,
)
from ...schemas.secret_schema import (
AWSSecrets,
AzureSecrets,
AnySecrets,
CloudSecretStatusSchema,
UserSecretResponseSchema,
)
Expand Down Expand Up @@ -116,7 +116,7 @@ async def update_password(


@router.get("/me/secrets")
async def get_user_secrets(
async def fetch_user_secrets(
current_user: UserModel = Depends(get_current_user), # noqa: B008
db: AsyncSession = Depends(async_get_db), # noqa: B008
) -> UserSecretResponseSchema:
Expand Down Expand Up @@ -170,115 +170,67 @@ async def get_user_secrets(
)


@router.post("/me/secrets/aws")
async def update_aws_secrets(
aws_secrets: AWSSecrets,
@router.post("/me/secrets")
async def update_user_secrets(
creds: AnySecrets,
current_user: UserModel = Depends(get_current_user), # noqa: B008
db: AsyncSession = Depends(async_get_db), # noqa: B008
) -> AWSUpdateSecretMessageSchema:
"""Update the current user's AWS secrets.
) -> MessageSchema:
"""Update the current user's secrets.

Args:
----
aws_secrets (AWSSecrets): The AWS credentials to store.
creds (AnySecrets): The provider credentials to store.
current_user (UserModel): The authenticated user.
db (Session): Database connection.

Returns:
-------
AWSUpdateSecretMessageSchema: Status message.
MessageSchema: Status message of updating user secrets.

"""
# Fetch secrets explicitly from the database
stmt = select(SecretModel).where(SecretModel.user_id == current_user.id)
result = await db.execute(stmt)
secrets = result.scalars().first()
# Fetch secrets from the database
secrets = await get_user_secrets(db, current_user.id)
if not secrets:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="User secrets record not found",
)

# Encrypt the AWS credentials using the user's public key
if not current_user.public_key:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User encryption keys not set up. Please register a new account.",
detail="User secrets record not found!",
)

# Convert to dictionary for encryption
aws_data = {
"aws_access_key": aws_secrets.aws_access_key,
"aws_secret_key": aws_secrets.aws_secret_key,
}

# Encrypt with the user's public key
encrypted_data = encrypt_with_public_key(aws_data, current_user.public_key)

# Update the secrets with encrypted values
secrets.aws_access_key = encrypted_data["aws_access_key"]
secrets.aws_secret_key = encrypted_data["aws_secret_key"]
secrets.aws_created_at = datetime.now(UTC)
await db.commit()

return AWSUpdateSecretMessageSchema(message="AWS credentials updated successfully")


@router.post("/me/secrets/azure")
async def update_azure_secrets(
azure_secrets: AzureSecrets,
current_user: UserModel = Depends(get_current_user), # noqa: B008
db: AsyncSession = Depends(async_get_db), # noqa: B008
) -> AzureUpdateSecretMessageSchema:
"""Update the current user's Azure secrets.

Args:
----
azure_secrets (AzureSecrets): The Azure credentials to store.
current_user (UserModel): The authenticated user.
db (Session): Database connection.
# Verify credentials are valid before storing
creds_obj = CredsFactory.create_creds_verification(credentials=creds)

Returns:
-------
AzureUpdateSecretMessageSchema: Success message.
verified, msg = creds_obj.verify_creds()

"""
# Fetch secrets explicitly from the database
stmt = select(SecretModel).where(SecretModel.user_id == current_user.id)
result = await db.execute(stmt)
secrets = result.scalars().first()
if not secrets:
if not verified:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="User secrets record not found",
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=msg.message,
)

# Encrypt the Azure credentials using the user's public key
# Encrypt the credentials using the user's public key
if not current_user.public_key:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User encryption keys not set up. Please register a new account.",
)

# Convert to dictionary for encryption
azure_data = {
"azure_client_id": azure_secrets.azure_client_id,
"azure_client_secret": azure_secrets.azure_client_secret,
"azure_tenant_id": azure_secrets.azure_tenant_id,
"azure_subscription_id": azure_secrets.azure_subscription_id,
}
# Convert provided credentials to dictionary for encryption
user_creds = creds_obj.get_user_creds()

# Encrypt with the user's public key
encrypted_data = encrypt_with_public_key(azure_data, current_user.public_key)
encrypted_data = encrypt_with_public_key(
data=user_creds, public_key_b64=current_user.public_key
)

# Update the secrets with encrypted values
secrets.azure_client_id = encrypted_data["azure_client_id"]
secrets.azure_client_secret = encrypted_data["azure_client_secret"]
secrets.azure_tenant_id = encrypted_data["azure_tenant_id"]
secrets.azure_subscription_id = encrypted_data["azure_subscription_id"]
secrets.azure_created_at = datetime.now(UTC)
await db.commit()

return AzureUpdateSecretMessageSchema(
message="Azure credentials updated successfully"
secrets = creds_obj.update_secret_schema(
secrets=secrets, encrypted_data=encrypted_data
)

# Add new secrets to database
await upsert_user_secrets(db, secrets, current_user.id)

return MessageSchema(
message=f"{creds.provider.value.upper()} credentials successfully verified and updated."
)
1 change: 1 addition & 0 deletions api/src/app/cloud/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Cloud controls for the OpenLabs API."""
163 changes: 163 additions & 0 deletions api/src/app/cloud/aws_creds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import logging
from datetime import UTC, datetime
from typing import List, Tuple

import boto3
from botocore.exceptions import ClientError

from src.app.schemas.message_schema import MessageSchema
from src.app.schemas.secret_schema import AWSSecrets, SecretSchema

from .base_creds import AbstractBaseCreds

# Configure logging
logger = logging.getLogger(__name__)


class AWSCreds(AbstractBaseCreds):
"""Credential verification for AWS."""

credentials: AWSSecrets

def __init__(self, credentials: AWSSecrets) -> None:
"""Initialize AWS credentials verification object."""
self.credentials = credentials

def get_user_creds(self) -> dict[str, str]:
"""Convert user AWS secrets to dictionary for encryption."""
return {
"aws_access_key": self.credentials.aws_access_key,
"aws_secret_key": self.credentials.aws_secret_key,
}

def update_secret_schema(
self, secrets: SecretSchema, encrypted_data: dict[str, str]
) -> SecretSchema:
"""Update user secrets schema with newly encrypted secrets."""
secrets.aws_access_key = encrypted_data["aws_access_key"]
secrets.aws_secret_key = encrypted_data["aws_secret_key"]
secrets.aws_created_at = datetime.now(UTC)
return secrets

def verify_creds(self) -> Tuple[bool, MessageSchema]:
"""Verify credentials authenticate to an AWS account."""
try:
# --- Step 1: Basic Authentication with STS ---
# Created shared session for authentication and IAM permission check
session = boto3.Session(
aws_access_key_id=self.credentials.aws_access_key,
aws_secret_access_key=self.credentials.aws_secret_key,
)
client = session.client("sts")
caller_identity = (
client.get_caller_identity()
) # will raise an error if not valid
caller_arn = caller_identity["Arn"]
logger.info(
"AWS credentials successfully authenticated for ARN: %s", caller_arn
)

if caller_arn.endswith(
":root"
): # If root access key credentials are used, skip permissions check as root user has all permissions
return (
True,
MessageSchema(
message="AWS credentials authenticated and all required permissions are present."
),
)

# --- Step 2: Simulate permissions for a sample of minimum critical actions ---
iam_client = session.client("iam")

actions_to_test = [
# For Instance
"ec2:RunInstances",
"ec2:TerminateInstances",
"ec2:DescribeInstances",
# For Vpc
"ec2:CreateVpc",
"ec2:DeleteVpc",
"ec2:DescribeVpcs",
# For Subnet
"ec2:CreateSubnet",
"ec2:DeleteSubnet",
"ec2:DescribeSubnets",
# For InternetGateway
"ec2:CreateInternetGateway",
"ec2:DeleteInternetGateway",
"ec2:AttachInternetGateway",
"ec2:DetachInternetGateway",
# For Eip and NatGateway
"ec2:AllocateAddress", # Create EIP
"ec2:ReleaseAddress", # Delete EIP
"ec2:AssociateAddress",
"ec2:CreateNatGateway",
"ec2:DeleteNatGateway",
# For KeyPair
"ec2:CreateKeyPair",
"ec2:DeleteKeyPair",
# For SecurityGroup and SecurityGroupRule
"ec2:CreateSecurityGroup",
"ec2:DeleteSecurityGroup",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:RevokeSecurityGroupIngress",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:RevokeSecurityGroupEgress",
# For RouteTable, Route, and RouteTableAssociation
"ec2:CreateRouteTable",
"ec2:DeleteRouteTable",
"ec2:CreateRoute",
"ec2:DeleteRoute",
"ec2:AssociateRouteTable",
"ec2:DisassociateRouteTable",
# For Transit Gateway ---
"ec2:CreateTransitGateway",
"ec2:DeleteTransitGateway",
"ec2:CreateTransitGatewayVpcAttachment",
"ec2:DeleteTransitGatewayVpcAttachment",
"ec2:CreateTransitGatewayRoute",
"ec2:DeleteTransitGatewayRoute",
"ec2:DescribeTransitGateways",
"ec2:DescribeTransitGatewayVpcAttachments",
]

simulation_results = iam_client.simulate_principal_policy(
PolicySourceArn=caller_arn, ActionNames=actions_to_test
)

# --- Step 3: Evaluate the simulation results ---
denied_actions: List[str] = []
for result in simulation_results["EvaluationResults"]:
if result["EvalDecision"] != "allowed":
denied_actions.append(result["EvalActionName"])

if denied_actions:
error_message = f"Authentication succeeded, but the user/group is missing required permissions. The following actions were denied: {', '.join(denied_actions)}"
logger.error(error_message)
return (
False,
MessageSchema(
message=f"Insufficient permissions for your AWS account user/group. Please ensure the following permissions are added: {', '.join(denied_actions)}"
),
)
logger.info("All simulated actions were allowed for ARN: %s", caller_arn)
return (
True,
MessageSchema(
message="AWS credentials authenticated and all required permissions are present."
),
)
except ClientError as e:
error_code = e.response["Error"]["Code"]
if error_code in ("InvalidClientTokenId", "SignatureDoesNotMatch"):
message = "AWS credentials could not be authenticated. Please ensure you are providing credentials that are linked to a valid AWS account."
elif error_code == "AccessDenied":
message = "AWS credentials are valid, but lack permissions to perform the permissions verification. Please ensure you give your AWS account user/group has the iam:SimulatePrincipalPolicy permission attached to a policy."
else:
message = e.response["Error"]["Message"]
logger.error("AWS verification failed: %s", message)
return (
False,
MessageSchema(message=message),
)
Loading