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

1873 saas connector friendbuy nextgen #2085

Merged
merged 11 commits into from
Jan 12, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The types of changes are:
* Dedicated folder for integrations.
* Adds support for Twilio email service (Sendgrid) [#2154](https://github.com/ethyca/fides/pull/2154)
* Access and erasure support for Recharge [#1709](https://github.com/ethyca/fides/pull/1709)
* Access and erasure support for Friendbuy Nextgen [#2085](https://github.com/ethyca/fides/pull/2085)

### Changed

Expand Down
48 changes: 48 additions & 0 deletions data/saas/config/friendbuy_nextgen_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
saas_config:
fides_key: <instance_fides_key>
name: Frienddbuy Nextgen SaaS Config
type: friendbuy_nextgen
description: A sample schema representing the Frienddbuy Nextgen connector for Fidesops
version: 0.0.1

connector_params:
- name: domain
- name: key
- name: secret

client_config:
protocol: https
host: <domain>
authentication:
strategy: friendbuy_nextgen
configuration:
key: <key>
secret: <secret>

test_request:
method: GET
path: /v1/user-data
query_params:
- name: email
value: test@test.com

endpoints:
- name: user
requests:
read:
method: GET
path: /v1/user-data
query_params:
- name: email
value: <email>
param_values:
- name: email
identity: email
delete:
request_override: friendbuy_nextgen_user_delete
param_values:
- name: email
references:
- dataset: <instance_fides_key>
field: user.emails
direction: from
42 changes: 42 additions & 0 deletions data/saas/dataset/friendbuy_nextgen_dataset.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
dataset:
- fides_key: friendbuy_nextgen_instance
name: Friendbuy Dataset
description: A sample dataset representing the Friendbuy connector for Fidesops
collections:
- name: user
fields:
- name: emails
data_categories: [user.contact.email]
fidesops_meta:
data_type: string[]
- name: names
data_categories: [user.name]
fidesops_meta:
data_type: string[]
- name: customerIds
data_categories: [system.operations]
fidesops_meta:
data_type: string[]
primary_key: True
- name: ipAddresses
data_categories: [user.device.ip_address]
fidesops_meta:
data_type: string[]
- name: languages
data_categories: [system.operations]
- name: userAgents
data_categories: [system.operations]
fidesops_meta:
data_type: string[]
- name: colorDepths
data_categories: [system.operations]
- name: platforms
data_categories: [system.operations]
- name: screenSizes
data_categories: [system.operations]
- name: trackedEvents
data_categories: [system.operations]
- name: shares
data_categories: [system.operations]
- name: conversions
data_categories: [system.operations]
6 changes: 6 additions & 0 deletions data/saas/saas_connector_registry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ dataset = "data/saas/dataset/doordash_dataset.yml"
icon = "data/saas/icon/doordash.svg"
human_readable = "Doordash"

[friendbuy_nextgen]
config = "data/saas/config/friendbuy_nextgen_config.yml"
dataset = "data/saas/dataset/friendbuy_nextgen_dataset.yml"
icon = "data/saas/icon/default.svg"
human_readable = "Friendbuy Nextgen"

[fullstory]
config = "data/saas/config/fullstory_config.yml"
dataset = "data/saas/dataset/fullstory_dataset.yml"
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ markers = [
"integration_datadog",
"integration_domo",
"integration_doordash",
"integration_friendbuy_nextgen",
"integration_fullstory",
"integration_segment",
"integration_sendgrid",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from datetime import datetime, timedelta
from typing import Dict, Optional, cast

from loguru import logger
from requests import PreparedRequest, post
from sqlalchemy.orm import Session

from fides.api.ops.common_exceptions import FidesopsException
from fides.api.ops.models.connectionconfig import ConnectionConfig
from fides.api.ops.schemas.saas.strategy_configuration import StrategyConfiguration
from fides.api.ops.service.authentication.authentication_strategy import (
AuthenticationStrategy,
)
from fides.api.ops.util.saas_util import assign_placeholders


class FriendbuyNextgenAuthenticationConfiguration(StrategyConfiguration):
"""
Parameters to authorize a Friendbuy Nextgen connection
"""

key: str
secret: str


class FriendbuyNextgenAuthenticationStrategy(AuthenticationStrategy):
"""
Generates a token from the provided key and secret.
Stores the expiration time to know when to refresh the token.
"""

name = "friendbuy_nextgen"
configuration_model = FriendbuyNextgenAuthenticationConfiguration

def __init__(self, configuration: FriendbuyNextgenAuthenticationConfiguration):
self.key = configuration.key
self.secret = configuration.secret

def add_authentication(
self, request: PreparedRequest, connection_config: ConnectionConfig
) -> PreparedRequest:
"""
Retrieves a token using the provided key and secret
"""

secrets = cast(Dict, connection_config.secrets)
domain: Optional[str] = secrets.get("domain")
token: Optional[str] = secrets.get("token")
expires_at: Optional[int] = secrets.get("expires_at")

if not token or self._close_to_expiration(expires_at, connection_config):
response = post(
url=f"https://{domain}/v1/authorization",
json={
"key": assign_placeholders(self.key, secrets),
"secret": assign_placeholders(self.secret, secrets),
},
)

if response.ok:
json_response = response.json()
token = json_response.get("token")
expires_at = int(
datetime.strptime(
json_response.get("expires"), "%Y-%m-%dT%H:%M:%S.%fZ"
).timestamp()
)

# merge the new values into the existing connection_config secrets
db = Session.object_session(connection_config)
updated_secrets = {
**secrets,
**{
"token": token,
"expires_at": expires_at,
},
}
connection_config.update(db, data={"secrets": updated_secrets})
logger.info(
"Successfully updated the token for {}",
connection_config.key,
)
else:
raise FidesopsException(f"Unable to get token {response.json()}")

request.headers["Authorization"] = f"Bearer {token}"
return request

@staticmethod
def _close_to_expiration(
expires_at: Optional[int], connection_config: ConnectionConfig
) -> bool:
"""Check if the access_token will expire in the next 10 minutes."""

if expires_at is None:
logger.info(
"The expires_at value is not defined for {}, skipping token refresh",
connection_config.key,
)
return False

return expires_at < (datetime.utcnow() + timedelta(minutes=10)).timestamp()
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import logging
from typing import Any, Dict, List

from fides.api.ops.models.policy import Policy
from fides.api.ops.models.privacy_request import PrivacyRequest
from fides.api.ops.schemas.saas.shared_schemas import HTTPMethod, SaaSRequestParams
from fides.api.ops.service.connectors.saas.authenticated_client import (
AuthenticatedClient,
)
from fides.api.ops.service.saas_request.saas_request_override_factory import (
SaaSRequestType,
register,
)
from fides.core.config import get_config

CONFIG = get_config()
logger = logging.getLogger(__name__)


@register("friendbuy_nextgen_user_delete", [SaaSRequestType.DELETE])
def friendbuy_nextgen_user_delete(
client: AuthenticatedClient,
param_values_per_row: List[Dict[str, Any]],
policy: Policy,
privacy_request: PrivacyRequest,
secrets: Dict[str, Any],
) -> int:
rows_deleted = 0
# each delete_params dict correspond to a record that needs to be deleted
for row_param_values in param_values_per_row:
# get params to be used in delete request
user_email = row_param_values.get("email")

# Since we get email like this ['<email>'], we convert it into <email> to proceed
user_email = str(user_email)[2:-2]
# check if the privacy_request targeted emails for erasure,
# if so rewrite with a format that can be accepted by friendbuy_nextgen
# regardless of the masking strategy in use

client.send(
SaaSRequestParams(
method=HTTPMethod.DELETE,
path="/v1/user-data",
query_params={"email": user_email},
)
)

rows_deleted += 1
return rows_deleted
Loading