Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create new validate endpoint for testing okta and aws credentials #722

Merged
merged 8 commits into from
Jun 8, 2022
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ The types of changes are:
* Add interaction for viewing a dataset collection
* Add column picker
* Okta, aws and database credentials can now come from `fidesctl.toml` config [#694](https://github.com/ethyca/fides/pull/694)
* New `validate` endpoint to test aws and okta credentials [#722](https://github.com/ethyca/fides/pull/722)


### Changed

Expand Down
3 changes: 2 additions & 1 deletion src/fidesapi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from fidesapi import view
from fidesapi.database import database
from fidesapi.database.database import get_db_health
from fidesapi.routes import crud, generate, visualize
from fidesapi.routes import crud, generate, validate, visualize
from fidesapi.routes.util import API_PREFIX, WEBAPP_DIRECTORY, WEBAPP_INDEX
from fidesapi.utils.errors import get_full_exception_name
from fidesapi.utils.logger import setup as setup_logging
Expand All @@ -38,6 +38,7 @@ def configure_routes() -> None:

app.include_router(view.router)
app.include_router(generate.router)
app.include_router(validate.router)


# Configure the routes here so we can generate the openapi json file
Expand Down
74 changes: 25 additions & 49 deletions src/fidesapi/routes/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@
from pydantic import BaseModel, root_validator

from fidesapi.routes.crud import get_resource
from fidesapi.routes.util import API_PREFIX, unobscure_string
from fidesapi.routes.util import (
API_PREFIX,
route_requires_aws_connector,
unobscure_aws_config,
)
from fidesapi.sql_models import sql_model_map
from fidesctl.connectors.models import (
AWSConfig,
ConnectorAuthFailureException,
OktaConfig,
)
from fidesctl.core.system import generate_aws_systems


Expand All @@ -33,24 +42,6 @@ class GenerateTypes(str, Enum):
DATASET = "datasets"


class AWSConfig(BaseModel):
"""
The model for the connection config for AWS
"""

region_name: str
aws_secret_access_key: str
aws_access_key_id: str


class OktaConfig(BaseModel):
"""
The model for the connection config for Okta
"""

okta_client_token: str


class Generate(BaseModel):
"""
Defines attributes for generating resources included in a request.
Expand Down Expand Up @@ -83,7 +74,7 @@ class GenerateRequestPayload(BaseModel):
generate: Generate


class GeneratedResponse(BaseModel):
class GenerateResponse(BaseModel):
"""
The model to house the response for generated infrastructure.
"""
Expand All @@ -97,7 +88,7 @@ class GeneratedResponse(BaseModel):
@router.post(
"/",
status_code=status.HTTP_200_OK,
response_model=GeneratedResponse,
response_model=GenerateResponse,
responses={
status.HTTP_400_BAD_REQUEST: {
"content": {
Expand All @@ -110,7 +101,7 @@ class GeneratedResponse(BaseModel):
)
async def generate(
generate_request_payload: GenerateRequestPayload, response: Response
) -> Dict:
) -> GenerateResponse:
"""
A multi-purpose endpoint to generate Fides resources based on existing
infrastructure
Expand All @@ -132,46 +123,31 @@ async def generate(
sql_model_map["organization"], generate_request_payload.organization_key
)
if generate_request_payload.generate.target.lower() == "aws":
try:
import boto3 # pylint: disable=unused-import
except ModuleNotFoundError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Packages not found, ensure aws is included: fidesctl[aws]",
)
log.info("Setting config for AWS")
generated_systems = generate_aws(
aws_config=AWSConfig(**generate_request_payload.generate.config.dict()),
organization=organization,
)
return {"generate_results": generated_systems}
return GenerateResponse(generate_results=generated_systems)


@route_requires_aws_connector
def generate_aws(
aws_config: AWSConfig, organization: Organization
) -> List[Dict[str, str]]:
"""
Returns a list of Systems found in AWS.
"""
from botocore.exceptions import ClientError

config = {
"region_name": aws_config.region_name,
"aws_access_key_id": unobscure_string(aws_config.aws_access_key_id),
"aws_secret_access_key": unobscure_string(aws_config.aws_secret_access_key),
}
log.info("Setting config for AWS")
unobscured_config = unobscure_aws_config(aws_config=aws_config)
earmenda marked this conversation as resolved.
Show resolved Hide resolved
try:
log.info("Generating systems from AWS")
aws_systems = generate_aws_systems(organization=organization, aws_config=config)
except ClientError as error:
if error.response["Error"]["Code"] in [
"InvalidClientTokenId",
"SignatureDoesNotMatch",
]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=error.response["Error"]["Message"],
)
raise error
aws_systems = generate_aws_systems(
organization=organization, aws_config=unobscured_config
)
except ConnectorAuthFailureException as error:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=str(error),
)

return [i.dict(exclude_none=True) for i in aws_systems]
67 changes: 66 additions & 1 deletion src/fidesapi/routes/util.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import zlib
from base64 import urlsafe_b64decode as b64d
from base64 import urlsafe_b64encode as b64e
from functools import update_wrapper
from pathlib import Path
from typing import Any, Callable

from fastapi import APIRouter
from fastapi import APIRouter, HTTPException, status

from fidesctl.connectors.models import AWSConfig, OktaConfig
from fidesctl.core.utils import API_PREFIX as _API_PREFIX

API_PREFIX = _API_PREFIX
Expand Down Expand Up @@ -33,3 +36,65 @@ def obscure_string(plaintext: str) -> str:
def unobscure_string(obscured: str) -> str:
"unobscures a string as a minor security measure"
return zlib.decompress(b64d(obscured.encode())).decode()


def unobscure_aws_config(aws_config: AWSConfig) -> AWSConfig:
"""
Given an aws config unobscures the access key id and
access key using the unobscure_string function.
"""
unobscured_config = AWSConfig(
region_name=aws_config.region_name,
aws_access_key_id=unobscure_string(aws_config.aws_access_key_id),
aws_secret_access_key=unobscure_string(aws_config.aws_secret_access_key),
)
return unobscured_config


def unobscure_okta_config(okta_config: OktaConfig) -> OktaConfig:
"""
Given an okta config unobscures the token using the
unobscure_string function.
"""
unobscured_config = OktaConfig(
orgUrl=okta_config.orgUrl, token=unobscure_string(okta_config.token)
)
return unobscured_config


def route_requires_aws_connector(func: Callable) -> Callable:
"""
Function decorator raises a bad request http exception if
required modules are not installed for the aws connector.
"""

def wrapper_func(*args, **kwargs) -> Any: # type: ignore
try:
import fidesctl.connectors.aws # pylint: disable=unused-import
except ModuleNotFoundError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Packages not found, ensure aws is included: fidesctl[aws]",
)
return func(*args, **kwargs)

return update_wrapper(wrapper_func, func)


def route_requires_okta_connector(func: Callable) -> Callable:
"""
Function decorator raises a bad request http exception if
required modules are not installed for the okta connector.
"""

def wrapper_func(*args, **kwargs) -> Any: # type: ignore
try:
import fidesctl.connectors.okta # pylint: disable=unused-import
except ModuleNotFoundError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Packages not found, ensure aws is included: fidesctl[okta]",
)
return func(*args, **kwargs)

return update_wrapper(wrapper_func, func)
120 changes: 120 additions & 0 deletions src/fidesapi/routes/validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""
Contains all of the endpoints required to validate credentials.
"""
from enum import Enum
from typing import Callable, Dict, Union

from fastapi import APIRouter, Response, status
from pydantic import BaseModel

from fidesapi.routes.util import (
API_PREFIX,
route_requires_aws_connector,
route_requires_okta_connector,
unobscure_aws_config,
unobscure_okta_config,
)
from fidesctl.connectors.models import (
AWSConfig,
ConnectorAuthFailureException,
ConnectorFailureException,
OktaConfig,
)


class ValidationTarget(str, Enum):
"""
Allowed targets for the validate endpoint
"""

AWS = "aws"
OKTA = "okta"


class ValidateRequest(BaseModel):
"""
Validate endpoint request object
"""

config: Union[AWSConfig, OktaConfig]
target: ValidationTarget


class ValidationStatus(str, Enum):
"""
Validate endpoint response status
"""

SUCCESS = "success"
FAILURE = "failure"


class ValidateResponse(BaseModel):
"""
Validate endpoint response object
"""

status: ValidationStatus
message: str


router = APIRouter(tags=["Validate"], prefix=f"{API_PREFIX}/validate")


@router.post(
"/",
status_code=status.HTTP_200_OK,
response_model=ValidateResponse,
)
async def validate(
validate_request_payload: ValidateRequest, response: Response
) -> ValidateResponse:
"""
Endpoint used to validate different resource targets.
"""
validate_function_map: Dict[ValidationTarget, Callable] = {
ValidationTarget.AWS: validate_aws,
ValidationTarget.OKTA: validate_okta,
}
validate_function = validate_function_map[validate_request_payload.target]

try:
await validate_function(validate_request_payload.config)
except ConnectorAuthFailureException as err:
return ValidateResponse(
status=ValidationStatus.FAILURE,
message=f"Authentication failed validating config. {str(err)}",
)
except ConnectorFailureException as err:
return ValidateResponse(
status=ValidationStatus.FAILURE,
message=f"Unexpected failure validating config. {str(err)}",
)

return ValidateResponse(
status=ValidationStatus.SUCCESS, message="Config validation succeeded"
)


@route_requires_aws_connector
async def validate_aws(aws_config: AWSConfig) -> None:
"""
Validates that given aws credentials are valid. Dependency
exception is raised if failure occurs.
"""
import fidesctl.connectors.aws as aws_connector

unobscured_config = unobscure_aws_config(aws_config=aws_config)
aws_connector.validate_credentials(aws_config=unobscured_config)


@route_requires_okta_connector
async def validate_okta(okta_config: OktaConfig) -> None:
"""
Validates that given okta credentials are valid. Dependency
exception is raised if failure occurs.
"""
import fidesctl.connectors.okta as okta_connector

unobscured_config = unobscure_okta_config(okta_config=okta_config)
await okta_connector.validate_credentials(okta_config=unobscured_config)
Loading