Skip to content

Commit

Permalink
Merge branch 'main' into feat/error-handler-workflow-6
Browse files Browse the repository at this point in the history
  • Loading branch information
topher-lo authored Jan 4, 2025
2 parents 4ca0bb5 + 3dffa62 commit bc57189
Show file tree
Hide file tree
Showing 19 changed files with 46 additions and 134 deletions.
5 changes: 1 addition & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ COMPOSE_PROJECT_NAME=tracecat

# --- Shared URL env vars ---
PUBLIC_APP_URL=http://localhost
PUBLIC_API_URL=http://localhost/api
PUBLIC_API_URL=${PUBLIC_APP_URL}/api
SAML_SP_ACS_URL=${PUBLIC_API_URL}/auth/saml/acs
INTERNAL_API_URL=http://api:8000
INTERNAL_EXECUTOR_URL=http://executor:8000
Expand Down Expand Up @@ -79,9 +79,6 @@ OAUTH_CLIENT_SECRET=
USER_AUTH_SECRET=your-auth-secret

# SAML SSO settings
SAML_IDP_ENTITY_ID=
SAML_IDP_REDIRECT_URL=
SAML_IDP_CERTIFICATE=
SAML_IDP_METADATA_URL=

# --- Temporal ---
Expand Down
4 changes: 2 additions & 2 deletions deployments/aws/ecs/ecs-temporal-ui.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ resource "aws_ecs_task_definition" "temporal_ui_task_definition" {
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
execution_role_arn = aws_iam_role.temporal_ui_execution.arn
task_role_arn = aws_iam_role.temporal_ui_task.arn
execution_role_arn = aws_iam_role.temporal_ui_execution[count.index].arn
task_role_arn = aws_iam_role.temporal_ui_task[count.index].arn

runtime_platform {
operating_system_family = "LINUX"
Expand Down
1 change: 0 additions & 1 deletion deployments/aws/ecs/iam.tf
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ resource "aws_iam_policy" "secrets_access" {
var.tracecat_signing_secret_arn,
var.oauth_client_id_arn,
var.oauth_client_secret_arn,
var.saml_idp_certificate_arn,
var.saml_idp_metadata_url_arn,
])
}
Expand Down
18 changes: 0 additions & 18 deletions deployments/aws/ecs/secrets.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ data "aws_secretsmanager_secret" "oauth_client_secret" {
arn = var.oauth_client_secret_arn
}

data "aws_secretsmanager_secret" "saml_idp_certificate" {
count = var.saml_idp_certificate_arn != null ? 1 : 0
arn = var.saml_idp_certificate_arn
}

data "aws_secretsmanager_secret" "saml_idp_metadata_url" {
count = var.saml_idp_metadata_url_arn != null ? 1 : 0
arn = var.saml_idp_metadata_url_arn
Expand Down Expand Up @@ -82,11 +77,6 @@ data "aws_secretsmanager_secret_version" "oauth_client_secret" {
secret_id = data.aws_secretsmanager_secret.oauth_client_secret[0].id
}

data "aws_secretsmanager_secret_version" "saml_idp_certificate" {
count = var.saml_idp_certificate_arn != null ? 1 : 0
secret_id = data.aws_secretsmanager_secret.saml_idp_certificate[0].id
}

data "aws_secretsmanager_secret_version" "saml_idp_metadata_url" {
count = var.saml_idp_metadata_url_arn != null ? 1 : 0
secret_id = data.aws_secretsmanager_secret.saml_idp_metadata_url[0].id
Expand Down Expand Up @@ -154,13 +144,6 @@ locals {
}
] : []

saml_idp_certificate_secret = var.saml_idp_certificate_arn != null ? [
{
name = "SAML_IDP_CERTIFICATE"
valueFrom = data.aws_secretsmanager_secret_version.saml_idp_certificate[0].arn
}
] : []

saml_idp_metadata_url_secret = var.saml_idp_metadata_url_arn != null ? [
{
name = "SAML_IDP_METADATA_URL"
Expand All @@ -186,7 +169,6 @@ locals {
local.tracecat_base_secrets,
local.oauth_client_id_secret,
local.oauth_client_secret_secret,
local.saml_idp_certificate_secret,
local.saml_idp_metadata_url_secret
)

Expand Down
6 changes: 0 additions & 6 deletions deployments/aws/ecs/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,6 @@ variable "oauth_client_secret_arn" {
default = null
}

variable "saml_idp_certificate_arn" {
type = string
description = "The ARN of the secret containing the SAML IDP certificate (optional)"
default = null
}

variable "saml_idp_metadata_url_arn" {
type = string
description = "The ARN of the secret containing the SAML IDP metadata URL (optional)"
Expand Down
1 change: 0 additions & 1 deletion deployments/aws/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ module "ecs" {
oauth_client_secret_arn = var.oauth_client_secret_arn

# SAML SSO
saml_idp_certificate_arn = var.saml_idp_certificate_arn
saml_idp_metadata_url_arn = var.saml_idp_metadata_url_arn

# Temporal UI authentication
Expand Down
6 changes: 0 additions & 6 deletions deployments/aws/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,6 @@ variable "oauth_client_secret_arn" {
default = null
}

variable "saml_idp_certificate_arn" {
type = string
description = "The ARN of the secret containing the SAML IDP certificate (optional)"
default = null
}

variable "saml_idp_metadata_url_arn" {
type = string
description = "The ARN of the secret containing the SAML IDP metadata URL (optional)"
Expand Down
1 change: 0 additions & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ services:
OAUTH_CLIENT_SECRET: ${OAUTH_CLIENT_SECRET}
USER_AUTH_SECRET: ${USER_AUTH_SECRET}
# SAML SSO
SAML_IDP_CERTIFICATE: ${SAML_IDP_CERTIFICATE}
SAML_IDP_METADATA_URL: ${SAML_IDP_METADATA_URL}
SAML_SP_ACS_URL: ${SAML_SP_ACS_URL}
# Temporal
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ services:
USER_AUTH_SECRET: ${USER_AUTH_SECRET}
RUN_MIGRATIONS: "true"
# SAML SSO
SAML_IDP_CERTIFICATE: ${SAML_IDP_CERTIFICATE}
SAML_IDP_METADATA_URL: ${SAML_IDP_METADATA_URL}
SAML_SP_ACS_URL: ${SAML_SP_ACS_URL}
# Temporal
Expand Down
10 changes: 0 additions & 10 deletions docs/self-hosting/authentication/saml.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,7 @@ TRACECAT__AUTH_TYPES=saml
</Step>
<Step title="Configure SAML settings in environment variables">
Set the following environment variables in your `.env` file:
- `SAML_IDP_CERTIFICATE`: Okta SAML X.509 certificate as text
- `SAML_IDP_METADATA_URL`: Okta metadata URL

<Note>
Do not include `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` in the `SAML_IDP_CERTIFICATE` environment variable.
</Note>
</Step>
<Step title="Restart Tracecat instance">
Restart Tracecat to apply the changes.
Expand All @@ -62,12 +57,7 @@ TRACECAT__AUTH_TYPES=saml
</Step>
<Step title="Configure SAML settings in environment variables">
Set the following environment variables in your `.env` file:
- `SAML_IDP_CERTIFICATE`: Microsoft Entra ID SAML Base64-encoded certificate as text
- `SAML_IDP_METADATA_URL`: Microsoft Entra ID metadata URL

<Note>
Do not include `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` in the `SAML_IDP_CERTIFICATE` environment variable.
</Note>
</Step>
<Step title="Restart Tracecat instance">
Restart Tracecat to apply the changes.
Expand Down
6 changes: 0 additions & 6 deletions docs/self-hosting/deployment-options/aws-ecs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@ please reach out to [[email protected]](mailto:[email protected]) for he
The variables are:
- `oauth_client_id_arn`
- `oauth_client_secret_arn`
- `saml_idp_entity_id_arn`
- `saml_idp_redirect_url_arn`
- `saml_idp_certificate_arn`
- `saml_idp_metadata_url_arn`

These variables are the ARNs of the AWS Secrets Manager secrets that contain the Google OAuth or SAML SSO configuration.
Expand All @@ -89,9 +86,6 @@ please reach out to [[email protected]](mailto:[email protected]) for he
export TF_VAR_oauth_client_secret_arn=<secret-arn>

# Or: Set SAML SSO variables
export TF_VAR_saml_idp_entity_id_arn=<secret-arn>
export TF_VAR_saml_idp_redirect_url_arn=<secret-arn>
export TF_VAR_saml_idp_certificate_arn=<secret-arn>
export TF_VAR_saml_idp_metadata_url_arn=<secret-arn>

# Create Terraform stack
Expand Down
12 changes: 2 additions & 10 deletions frontend/src/client/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,12 @@ export const $ActionRead = {
type: 'object',
title: 'Inputs'
},
key: {
type: 'string',
title: 'Key'
},
control_flow: {
'$ref': '#/components/schemas/ActionControlFlow'
}
},
type: 'object',
required: ['id', 'type', 'title', 'description', 'status', 'inputs', 'key'],
required: ['id', 'type', 'title', 'description', 'status', 'inputs'],
title: 'ActionRead'
} as const;

Expand Down Expand Up @@ -143,14 +139,10 @@ export const $ActionReadMinimal = {
status: {
type: 'string',
title: 'Status'
},
key: {
type: 'string',
title: 'Key'
}
},
type: 'object',
required: ['id', 'workflow_id', 'type', 'title', 'description', 'status', 'key'],
required: ['id', 'workflow_id', 'type', 'title', 'description', 'status'],
title: 'ActionReadMinimal'
} as const;

Expand Down
2 changes: 0 additions & 2 deletions frontend/src/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export type ActionRead = {
inputs: {
[key: string]: unknown;
};
key: string;
control_flow?: ActionControlFlow;
};

Expand All @@ -42,7 +41,6 @@ export type ActionReadMinimal = {
title: string;
description: string;
status: string;
key: string;
};

export type ActionRetryPolicy = {
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ dependencies = [
"authlib>=1.3.1,<1.4.0",
"cloudpickle==3.0.0",
"colorlog==6.8.2",
"croniter==2.0.5",
"cryptography==43.0.1",
"email-validator>=2.0.0",
"fastapi-users[sqlalchemy,oauth]==13.0.0",
Expand Down
13 changes: 8 additions & 5 deletions tracecat/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,17 +195,20 @@ def create_app(**kwargs) -> FastAPI:
prefix="/auth/oauth",
tags=["auth"],
)

if AuthType.SAML in config.TRACECAT__AUTH_TYPES:
from tracecat.auth.saml import router as saml_router

logger.info("SAML auth type enabled")
app.include_router(saml_router)

if AuthType.BASIC not in config.TRACECAT__AUTH_TYPES:
# Need basic auth router for `logout` endpoint
app.include_router(
fastapi_users.get_logout_router(auth_backend),
prefix="/auth",
tags=["auth"],
)
if AuthType.SAML in config.TRACECAT__AUTH_TYPES:
from tracecat.auth.saml import router as saml_router

logger.info("SAML auth type enabled")
app.include_router(saml_router)

# Exception handlers
app.add_exception_handler(Exception, generic_exception_handler)
Expand Down
70 changes: 32 additions & 38 deletions tracecat/auth/saml.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import tempfile
import xml.etree.ElementTree as ET
from dataclasses import asdict, dataclass
from typing import Annotated, Any
Expand All @@ -12,7 +11,6 @@

from tracecat.auth.users import AuthBackendStrategyDep, UserManagerDep, auth_backend
from tracecat.config import (
SAML_IDP_CERTIFICATE,
SAML_IDP_METADATA_URL,
SAML_SP_ACS_URL,
TRACECAT__PUBLIC_API_URL,
Expand All @@ -33,7 +31,6 @@ class SAMLAttribute:

name: str
value: str
name_format: str = ""


class SAMLParser:
Expand All @@ -57,20 +54,14 @@ def _register_namespaces(self):

def _extract_attribute(self, attribute_elem: ET.Element) -> SAMLAttribute:
"""Extract a single SAML attribute from an XML element"""

name = attribute_elem.get("Name")
name_format = attribute_elem.get("NameFormat")
value_elem = attribute_elem.find("saml2:AttributeValue", self.NAMESPACES)

if not name:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="SAML response failed: AttributeName is empty",
)

if not name_format:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="SAML response failed: AttributeNameFormat is empty",
detail=f"SAML response failed: AttributeName for {attribute_elem} is empty",
)

if value_elem is None:
Expand All @@ -86,7 +77,7 @@ def _extract_attribute(self, attribute_elem: ET.Element) -> SAMLAttribute:
detail=f"SAML response failed: AttributeValue for {name} is empty",
)

return SAMLAttribute(name=name, value=value_text, name_format=name_format)
return SAMLAttribute(name=name, value=value_text)

def get_attribute_value(self, attribute_name: str) -> str:
"""Helper method to easily get an attribute value"""
Expand Down Expand Up @@ -152,34 +143,22 @@ def create_saml_client() -> Saml2Client:
"want_response_signed": False,
},
},
}
# Save the cert to a temporary file
with tempfile.NamedTemporaryFile(mode="w+", suffix=".crt") as tmp_file:
if not SAML_IDP_CERTIFICATE:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="SAML SSO certificate has not been configured.",
)
tmp_file.write(
f"-----BEGIN CERTIFICATE-----\n{SAML_IDP_CERTIFICATE}\n-----END CERTIFICATE-----\n"
)
tmp_file.flush()
saml_settings["metadata"] = {
"metadata": {
"remote": [
{
"url": SAML_IDP_METADATA_URL,
"cert": tmp_file.name, # Path to cert
}
]
}
try:
config = Saml2Config()
config.load(saml_settings)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to load SAML configuration",
) from e
},
}
try:
config = Saml2Config()
config.load(saml_settings)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to load SAML configuration",
) from e

client = Saml2Client(config)
return client
Expand Down Expand Up @@ -220,13 +199,28 @@ async def sso_acs(
saml_response, BINDING_HTTP_POST
)
parser = SAMLParser(str(authn_response))
email = parser.get_attribute_value("email")

# Validate email
# Try to get the email from SAML attributes
email = (
parser.get_attribute_value("email")
# Okta
or parser.get_attribute_value(
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
)
# Microsoft Entra ID
or parser.get_attribute_value(
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
)
or parser.get_attribute_value(
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
)
)

if not email:
attributes = parser.attributes or {}
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email not found in the SAML response.",
detail=f"Expected attribute 'email' in the SAML response, but got: {list(attributes.keys())}",
)

# Try to get the user from the database
Expand Down
Loading

0 comments on commit bc57189

Please sign in to comment.