diff --git a/datahub-web-react/src/app/ingest/source/builder/RecipeForm/__tests__/utils.test.ts b/datahub-web-react/src/app/ingest/source/builder/RecipeForm/__tests__/utils.test.ts new file mode 100644 index 00000000000000..160074d94abac4 --- /dev/null +++ b/datahub-web-react/src/app/ingest/source/builder/RecipeForm/__tests__/utils.test.ts @@ -0,0 +1,26 @@ +import { validateURL } from '../../../utils'; + +describe('validateURL function', () => { + it('should resolve if the URL is valid', async () => { + const validator = validateURL('test'); + await expect(validator.validator(null, 'https://example.com')).resolves.toBeUndefined(); + await expect(validator.validator(null, 'http://example.com')).resolves.toBeUndefined(); + await expect(validator.validator(null, 'http://subdomain.example.com/path')).resolves.toBeUndefined(); + }); + + it('should reject if the URL is invalid', async () => { + const validator = validateURL('test url'); + await expect(validator.validator(null, 'http://example')).rejects.toThrowError('A valid test url is required.'); + await expect(validator.validator(null, 'example')).rejects.toThrowError('A valid test url is required.'); + await expect(validator.validator(null, 'http://example')).rejects.toThrowError( + 'A valid test url is required.', + ); + }); + + it('should resolve if the value is empty', async () => { + const validator = validateURL('test'); + await expect(validator.validator(null, '')).resolves.toBeUndefined(); + await expect(validator.validator(null, undefined)).resolves.toBeUndefined(); + await expect(validator.validator(null, null)).resolves.toBeUndefined(); + }); +}); diff --git a/datahub-web-react/src/app/ingest/source/builder/RecipeForm/azure.ts b/datahub-web-react/src/app/ingest/source/builder/RecipeForm/azure.ts new file mode 100644 index 00000000000000..9dfbaaae0a26dd --- /dev/null +++ b/datahub-web-react/src/app/ingest/source/builder/RecipeForm/azure.ts @@ -0,0 +1,171 @@ +import { validateURL } from '../../utils'; +import { RecipeField, FieldType, setListValuesOnRecipe } from './common'; + +export const AZURE_CLIENT_ID: RecipeField = { + name: 'client_id', + label: 'Client ID', + tooltip: 'Application ID. Found in your app registration on Azure AD Portal', + type: FieldType.TEXT, + fieldPath: 'source.config.client_id', + placeholder: '00000000-0000-0000-0000-000000000000', + required: true, + rules: null, +}; + +export const AZURE_TENANT_ID: RecipeField = { + name: 'tenant_id', + label: 'Tenant ID', + tooltip: 'Directory ID. Found in your app registration on Azure AD Portal', + type: FieldType.TEXT, + fieldPath: 'source.config.tenant_id', + placeholder: '00000000-0000-0000-0000-000000000000', + required: true, + rules: null, +}; + +export const AZURE_CLIENT_SECRET: RecipeField = { + name: 'client_secret', + label: 'Client Secret', + tooltip: 'The Azure client secret.', + type: FieldType.SECRET, + fieldPath: 'source.config.client_secret', + placeholder: '00000000-0000-0000-0000-000000000000', + required: true, + rules: null, +}; + +export const AZURE_REDIRECT_URL: RecipeField = { + name: 'redirect', + label: 'Redirect URL', + tooltip: 'Redirect URL. Found in your app registration on Azure AD Portal.', + type: FieldType.TEXT, + fieldPath: 'source.config.redirect', + placeholder: 'https://login.microsoftonline.com/common/oauth2/nativeclient', + required: true, + rules: [() => validateURL('Redirect URI')], +}; + +export const AZURE_AUTHORITY_URL: RecipeField = { + name: 'authority', + label: 'Authority URL', + tooltip: 'Is a URL that indicates a directory that MSAL can request tokens from..', + type: FieldType.TEXT, + fieldPath: 'source.config.authority', + placeholder: 'https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000', + required: true, + rules: [() => validateURL('Azure authority URL')], +}; + +export const AZURE_TOKEN_URL: RecipeField = { + name: 'token_url', + label: 'Token URL', + tooltip: + 'The token URL that acquires a token from Azure AD for authorizing requests. This source will only work with v1.0 endpoint.', + type: FieldType.TEXT, + fieldPath: 'source.config.token_url', + placeholder: 'https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/oauth2/token', + required: true, + rules: [() => validateURL('Azure token URL')], +}; + +export const AZURE_GRAPH_URL: RecipeField = { + name: 'graph_url', + label: 'Graph URL', + tooltip: 'Microsoft Graph API endpoint', + type: FieldType.TEXT, + fieldPath: 'source.config.graph_url', + placeholder: 'https://graph.microsoft.com/v1.0', + required: true, + rules: [() => validateURL('Graph url URL')], +}; + +export const AZURE_INGEST_USERS: RecipeField = { + name: 'ingest_users', + label: 'Ingest Users', + tooltip: 'Flag to determine whether to ingest users from Azure AD or not.', + type: FieldType.BOOLEAN, + fieldPath: 'source.config.ingest_users', + rules: null, +}; + +export const AZURE_INGEST_GROUPS: RecipeField = { + name: 'ingest_groups', + label: 'Ingest Groups', + tooltip: 'Flag to determine whether to ingest groups from Azure AD or not.', + type: FieldType.BOOLEAN, + fieldPath: 'source.config.ingest_groups', + rules: null, +}; + +const schemaAllowFieldPathGroup = 'source.config.groups_pattern.allow'; +export const GROUP_ALLOW: RecipeField = { + name: 'groups.allow', + label: 'Allow Patterns', + tooltip: + 'Only include specific schemas by providing the name of a schema, or a regular expression (regex) to include specific schemas. If not provided, all schemas inside allowed databases will be included.', + placeholder: 'group_pattern', + type: FieldType.LIST, + buttonLabel: 'Add pattern', + fieldPath: schemaAllowFieldPathGroup, + rules: null, + section: 'Group', + setValueOnRecipeOverride: (recipe: any, values: string[]) => + setListValuesOnRecipe(recipe, values, schemaAllowFieldPathGroup), +}; + +const schemaDenyFieldPathGroup = 'source.config.groups_pattern.deny'; +export const GROUP_DENY: RecipeField = { + name: 'groups.deny', + label: 'Deny Patterns', + tooltip: + 'Exclude specific schemas by providing the name of a schema, or a regular expression (regex). If not provided, all schemas inside allowed databases will be included. Deny patterns always take precedence over allow patterns.', + placeholder: 'user_pattern', + type: FieldType.LIST, + buttonLabel: 'Add pattern', + fieldPath: schemaDenyFieldPathGroup, + rules: null, + section: 'Group', + setValueOnRecipeOverride: (recipe: any, values: string[]) => + setListValuesOnRecipe(recipe, values, schemaDenyFieldPathGroup), +}; + +const schemaAllowFieldPathUser = 'source.config.users_pattern.allow'; +export const USER_ALLOW: RecipeField = { + name: 'user.allow', + label: 'Allow Patterns', + tooltip: + 'Exclude specific schemas by providing the name of a schema, or a regular expression (regex). If not provided, all schemas inside allowed databases will be included. Deny patterns always take precedence over allow patterns.', + placeholder: 'user_pattern', + type: FieldType.LIST, + buttonLabel: 'Add pattern', + fieldPath: schemaAllowFieldPathUser, + rules: null, + section: 'User', + setValueOnRecipeOverride: (recipe: any, values: string[]) => + setListValuesOnRecipe(recipe, values, schemaAllowFieldPathUser), +}; + +const schemaDenyFieldPathUser = 'source.config.users_pattern.deny'; +export const USER_DENY: RecipeField = { + name: 'user.deny', + label: 'Deny Patterns', + tooltip: + 'Exclude specific schemas by providing the name of a schema, or a regular expression (regex). If not provided, all schemas inside allowed databases will be included. Deny patterns always take precedence over allow patterns.', + placeholder: 'user_pattern', + type: FieldType.LIST, + buttonLabel: 'Add pattern', + fieldPath: schemaDenyFieldPathUser, + rules: null, + section: 'User', + setValueOnRecipeOverride: (recipe: any, values: string[]) => + setListValuesOnRecipe(recipe, values, schemaDenyFieldPathUser), +}; + +export const SKIP_USERS_WITHOUT_GROUP: RecipeField = { + name: 'skip_users_without_a_group', + label: 'Skip users without group', + tooltip: 'Whether to skip users without group from Okta.', + type: FieldType.BOOLEAN, + fieldPath: 'source.config.skip_users_without_a_group', + rules: null, +}; diff --git a/datahub-web-react/src/app/ingest/source/builder/RecipeForm/constants.ts b/datahub-web-react/src/app/ingest/source/builder/RecipeForm/constants.ts index 844bf50926764a..6a5e6c9de2b96b 100644 --- a/datahub-web-react/src/app/ingest/source/builder/RecipeForm/constants.ts +++ b/datahub-web-react/src/app/ingest/source/builder/RecipeForm/constants.ts @@ -83,7 +83,7 @@ import { PROJECT_NAME, } from './lookml'; import { PRESTO, PRESTO_HOST_PORT, PRESTO_DATABASE, PRESTO_USERNAME, PRESTO_PASSWORD } from './presto'; -import { BIGQUERY_BETA, CSV, DBT_CLOUD, MYSQL, POWER_BI, UNITY_CATALOG, VERTICA } from '../constants'; +import { AZURE, BIGQUERY_BETA, CSV, DBT_CLOUD, MYSQL, OKTA, POWER_BI, UNITY_CATALOG, VERTICA } from '../constants'; import { BIGQUERY_BETA_PROJECT_ID, DATASET_ALLOW, DATASET_DENY, PROJECT_ALLOW, PROJECT_DENY } from './bigqueryBeta'; import { MYSQL_HOST_PORT, MYSQL_PASSWORD, MYSQL_USERNAME } from './mysql'; import { MSSQL, MSSQL_DATABASE, MSSQL_HOST_PORT, MSSQL_PASSWORD, MSSQL_USERNAME } from './mssql'; @@ -141,6 +141,36 @@ import { INCLUDE_PROJECTIONS_LINEAGE, } from './vertica'; import { CSV_ARRAY_DELIMITER, CSV_DELIMITER, CSV_FILE_URL, CSV_WRITE_SEMANTICS } from './csv'; +import { + INCLUDE_DEPROVISIONED_USERS, + INCLUDE_SUSPENDED_USERS, + INGEST_GROUPS, + INGEST_USERS, + OKTA_API_TOKEN, + OKTA_DOMAIN_URL, + POFILE_TO_GROUP, + POFILE_TO_GROUP_REGX_ALLOW, + POFILE_TO_GROUP_REGX_DENY, + POFILE_TO_USER, + POFILE_TO_USER_REGX_ALLOW, + POFILE_TO_USER_REGX_DENY, + SKIP_USERS_WITHOUT_GROUP, +} from './okta'; +import { + AZURE_AUTHORITY_URL, + AZURE_CLIENT_ID, + AZURE_CLIENT_SECRET, + AZURE_GRAPH_URL, + AZURE_INGEST_GROUPS, + AZURE_INGEST_USERS, + AZURE_REDIRECT_URL, + AZURE_TENANT_ID, + AZURE_TOKEN_URL, + GROUP_ALLOW, + GROUP_DENY, + USER_ALLOW, + USER_DENY, +} from './azure'; export enum RecipeSections { Connection = 0, @@ -459,6 +489,36 @@ export const RECIPE_FIELDS: RecipeFields = { filterFields: [], advancedFields: [CSV_ARRAY_DELIMITER, CSV_DELIMITER, CSV_WRITE_SEMANTICS], }, + [OKTA]: { + fields: [OKTA_DOMAIN_URL, OKTA_API_TOKEN, POFILE_TO_USER, POFILE_TO_GROUP], + filterFields: [ + POFILE_TO_USER_REGX_ALLOW, + POFILE_TO_USER_REGX_DENY, + POFILE_TO_GROUP_REGX_ALLOW, + POFILE_TO_GROUP_REGX_DENY, + ], + advancedFields: [ + INGEST_USERS, + INGEST_GROUPS, + INCLUDE_DEPROVISIONED_USERS, + INCLUDE_SUSPENDED_USERS, + STATEFUL_INGESTION_ENABLED, + SKIP_USERS_WITHOUT_GROUP, + ], + }, + [AZURE]: { + fields: [ + AZURE_CLIENT_ID, + AZURE_TENANT_ID, + AZURE_CLIENT_SECRET, + AZURE_REDIRECT_URL, + AZURE_AUTHORITY_URL, + AZURE_TOKEN_URL, + AZURE_GRAPH_URL, + ], + filterFields: [GROUP_ALLOW, GROUP_DENY, USER_ALLOW, USER_DENY], + advancedFields: [AZURE_INGEST_USERS, AZURE_INGEST_GROUPS, STATEFUL_INGESTION_ENABLED, SKIP_USERS_WITHOUT_GROUP], + }, }; export const CONNECTORS_WITH_FORM = new Set(Object.keys(RECIPE_FIELDS)); diff --git a/datahub-web-react/src/app/ingest/source/builder/RecipeForm/csv.ts b/datahub-web-react/src/app/ingest/source/builder/RecipeForm/csv.ts index fba4f3b9d01641..2cb3e7edc94d70 100644 --- a/datahub-web-react/src/app/ingest/source/builder/RecipeForm/csv.ts +++ b/datahub-web-react/src/app/ingest/source/builder/RecipeForm/csv.ts @@ -1,18 +1,6 @@ +import { validateURL } from '../../utils'; import { RecipeField, FieldType } from './common'; -const validateURL = (fieldName) => { - return { - validator(_, value) { - const URLPattern = new RegExp(/^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!$&'()*+,;=.]+$/); - const isURLValid = URLPattern.test(value); - if (!value || isURLValid) { - return Promise.resolve(); - } - return Promise.reject(new Error(`A valid ${fieldName} is required.`)); - }, - }; -}; - export const CSV_FILE_URL: RecipeField = { name: 'filename', label: 'File URL', diff --git a/datahub-web-react/src/app/ingest/source/builder/RecipeForm/okta.ts b/datahub-web-react/src/app/ingest/source/builder/RecipeForm/okta.ts new file mode 100644 index 00000000000000..6efee3769f908d --- /dev/null +++ b/datahub-web-react/src/app/ingest/source/builder/RecipeForm/okta.ts @@ -0,0 +1,153 @@ +import { validateURL } from '../../utils'; +import { RecipeField, FieldType, setListValuesOnRecipe } from './common'; + +export const OKTA_DOMAIN_URL: RecipeField = { + name: 'okta_domain', + label: 'Okta Domain URL', + tooltip: 'The location of your Okta Domain, without a protocol.', + type: FieldType.TEXT, + fieldPath: 'source.config.okta_domain', + placeholder: 'dev-35531955.okta.com', + required: true, + rules: [() => validateURL('Okta Domain URL')], +}; + +export const OKTA_API_TOKEN: RecipeField = { + name: 'credential.project_id', + label: 'Token', + tooltip: 'An API token generated for the DataHub application inside your Okta Developer Console.', + type: FieldType.SECRET, + fieldPath: 'source.config.okta_api_token', + placeholder: 'd0121d0000882411234e11166c6aaa23ed5d74e0', + rules: null, + required: true, +}; + +export const POFILE_TO_USER: RecipeField = { + name: 'email', + label: 'Okta Email', + tooltip: + 'Which Okta User Profile attribute to use as input to DataHub username mapping. Common values used are - login, email.', + type: FieldType.TEXT, + fieldPath: 'source.config.okta_profile_to_username_attr', + placeholder: 'email', + rules: null, +}; + +export const POFILE_TO_GROUP: RecipeField = { + name: 'okta_profile_to_group_name_attr', + label: 'Okta Profile to group name attribute', + tooltip: 'Which Okta Group Profile attribute to use as input to DataHub group name mapping.', + type: FieldType.TEXT, + fieldPath: 'source.config.okta_profile_to_group_name_attr', + placeholder: 'Group name', + rules: null, +}; + +const schemaAllowFieldPath = 'source.config.okta_profile_to_username_regex.allow'; +export const POFILE_TO_USER_REGX_ALLOW: RecipeField = { + name: 'user.allow', + label: 'Allow Patterns', + tooltip: + 'Only include specific schemas by providing the name of a schema, or a regular expression (regex) to include specific schemas. If not provided, all schemas inside allowed databases will be included.', + placeholder: 'user_pattern', + type: FieldType.LIST, + buttonLabel: 'Add pattern', + fieldPath: schemaAllowFieldPath, + rules: null, + section: 'Okta Profile To User Attribute Regex', + setValueOnRecipeOverride: (recipe: any, values: string[]) => + setListValuesOnRecipe(recipe, values, schemaAllowFieldPath), +}; + +const schemaDenyFieldPath = 'source.config.okta_profile_to_username_regex.deny'; +export const POFILE_TO_USER_REGX_DENY: RecipeField = { + name: 'user.deny', + label: 'Deny Patterns', + tooltip: + 'Only include specific schemas by providing the name of a schema, or a regular expression (regex) to include specific schemas. If not provided, all schemas inside allowed databases will be included.', + placeholder: 'user_pattern', + type: FieldType.LIST, + buttonLabel: 'Add pattern', + fieldPath: schemaDenyFieldPath, + rules: null, + section: 'Okta Profile To User Attribute Regex', + setValueOnRecipeOverride: (recipe: any, values: string[]) => + setListValuesOnRecipe(recipe, values, schemaDenyFieldPath), +}; + +const schemaAllowFieldPathForGroup = 'source.config.okta_profile_to_group_name_regex.allow'; +export const POFILE_TO_GROUP_REGX_ALLOW: RecipeField = { + name: 'group.allow', + label: 'Allow Patterns', + tooltip: + 'Only include specific schemas by providing the name of a schema, or a regular expression (regex) to include specific schemas. If not provided, all schemas inside allowed databases will be included.', + placeholder: 'group_pattern', + type: FieldType.LIST, + buttonLabel: 'Add pattern', + fieldPath: schemaAllowFieldPathForGroup, + rules: null, + section: 'Okta Profile To Group Attribute Regex', + setValueOnRecipeOverride: (recipe: any, values: string[]) => + setListValuesOnRecipe(recipe, values, schemaAllowFieldPathForGroup), +}; + +const schemaDenyFieldPathForGroup = 'source.config.okta_profile_to_group_name_regex.deny'; +export const POFILE_TO_GROUP_REGX_DENY: RecipeField = { + name: 'group.deny', + label: 'Deny Patterns', + tooltip: + 'Only include specific schemas by providing the name of a schema, or a regular expression (regex) to include specific schemas. If not provided, all schemas inside allowed databases will be included.', + placeholder: 'group_pattern', + type: FieldType.LIST, + buttonLabel: 'Add pattern', + fieldPath: schemaDenyFieldPathForGroup, + rules: null, + section: 'Okta Profile To Group Attribute Regex', + setValueOnRecipeOverride: (recipe: any, values: string[]) => + setListValuesOnRecipe(recipe, values, schemaDenyFieldPathForGroup), +}; +export const INGEST_USERS: RecipeField = { + name: 'ingest_users', + label: 'Ingest Users', + tooltip: 'Whether users should be ingested into DataHub.', + type: FieldType.BOOLEAN, + fieldPath: 'source.config.ingest_users', + rules: null, +}; + +export const INGEST_GROUPS: RecipeField = { + name: 'ingest_groups', + label: 'Ingest Groups', + tooltip: 'Whether groups should be ingested into DataHub.', + type: FieldType.BOOLEAN, + fieldPath: 'source.config.ingest_groups', + rules: null, +}; + +export const INCLUDE_DEPROVISIONED_USERS: RecipeField = { + name: 'include_deprovisioned_users', + label: 'Include deprovisioned users', + tooltip: 'Whether to ingest users in the DEPROVISIONED state from Okta.', + type: FieldType.BOOLEAN, + fieldPath: 'source.config.include_deprovisioned_users', + rules: null, +}; +export const INCLUDE_SUSPENDED_USERS: RecipeField = { + name: 'include_suspended_users', + label: 'Include suspended users', + tooltip: 'Whether to ingest users in the SUSPENDED state from Okta.', + type: FieldType.BOOLEAN, + fieldPath: 'source.config.include_suspended_users', + rules: null, +}; + +export const SKIP_USERS_WITHOUT_GROUP: RecipeField = { + name: 'skip_users_without_a_group', + label: 'Skip users without group', + tooltip: 'Whether to skip users without group from Okta.', + type: FieldType.BOOLEAN, + fieldPath: 'source.config.skip_users_without_a_group', + rules: null, +}; + diff --git a/datahub-web-react/src/app/ingest/source/builder/sources.json b/datahub-web-react/src/app/ingest/source/builder/sources.json index f4bfd45eb4f837..5d004abfa78d83 100644 --- a/datahub-web-react/src/app/ingest/source/builder/sources.json +++ b/datahub-web-react/src/app/ingest/source/builder/sources.json @@ -200,14 +200,14 @@ "name": "azure-ad", "displayName": "Azure AD", "docsUrl": "https://datahubproject.io/docs/generated/ingestion/sources/azure-ad/", - "recipe": "source:\n type: azure-ad\n config:\n client_id: # Your Azure Client ID, e.g. \"00000000-0000-0000-0000-000000000000\"\n tenant_id: # Your Azure Tenant ID, e.g. \"00000000-0000-0000-0000-000000000000\"\n # Add secret in Secrets Tab with this name\n client_secret: \"${AZURE_AD_CLIENT_SECRET}\"\n redirect: # Your Redirect URL, e.g. \"https://login.microsoftonline.com/common/oauth2/nativeclient\"\n authority: # Your Authority URL, e.g. \"https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000\"\n token_url: # Your Token URL, e.g. \"https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/oauth2/token\"\n graph_url: # The Graph URL, e.g. \"https://graph.microsoft.com/v1.0\"\n \n # Optional flags to ingest users, groups, or both\n ingest_users: True\n ingest_groups: True\n \n # Optional Allow / Deny extraction of particular Groups\n # groups_pattern:\n # allow:\n # - \".*\"\n\n # Optional Allow / Deny extraction of particular Users.\n # users_pattern:\n # allow:\n # - \".*\"" + "recipe": "source:\n type: azure-ad\n config:\n client_id: # Your Azure Client ID, e.g. \"00000000-0000-0000-0000-000000000000\"\n tenant_id: # Your Azure Tenant ID, e.g. \"00000000-0000-0000-0000-000000000000\"\n # Add secret in Secrets Tab with this name\n client_secret: \n redirect: # Your Redirect URL, e.g. \"https://login.microsoftonline.com/common/oauth2/nativeclient\"\n authority: # Your Authority URL, e.g. \"https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000\"\n token_url: # Your Token URL, e.g. \"https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/oauth2/token\"\n graph_url: # The Graph URL, e.g. \"https://graph.microsoft.com/v1.0\"\n \n # Optional flags to ingest users, groups, or both\n ingest_users: True\n ingest_groups: True\n \n # Optional Allow / Deny extraction of particular Groups\n # groups_pattern:\n # allow:\n # - \".*\"\n\n # Optional Allow / Deny extraction of particular Users.\n # users_pattern:\n # allow:\n # - \".*\"" }, { "urn": "urn:li:dataPlatform:okta", "name": "okta", "displayName": "Okta", "docsUrl": "https://datahubproject.io/docs/generated/ingestion/sources/okta/", - "recipe": "source:\n type: okta\n config:\n # Coordinates\n okta_domain: # Your Okta Domain, e.g. \"dev-35531955.okta.com\"\n\n # Credentials\n # Add secret in Secrets Tab with relevant names for each variable\n okta_api_token: \"${OKTA_API_TOKEN}\" # Your Okta API Token, e.g. \"11be4R_M2MzDqXawbTHfKGpKee0kuEOfX1RCQSRx99\"\n\n # Optional flags to ingest users, groups, or both\n ingest_users: True\n ingest_groups: True\n\n # Optional: Customize the mapping to DataHub Username from an attribute appearing in the Okta User\n # profile. Reference: https://developer.okta.com/docs/reference/api/users/\n # okta_profile_to_username_attr: str = \"login\"\n # okta_profile_to_username_regex: str = \"([^@]+)\"\n \n # Optional: Customize the mapping to DataHub Group from an attribute appearing in the Okta Group\n # profile. Reference: https://developer.okta.com/docs/reference/api/groups/\n # okta_profile_to_group_name_attr: str = \"name\"\n # okta_profile_to_group_name_regex: str = \"(.*)\"\n \n # Optional: Include deprovisioned or suspended Okta users in the ingestion.\n # include_deprovisioned_users = False\n # include_suspended_users = False" + "recipe": "source:\n type: okta\n config:\n # Coordinates\n okta_domain: # Your Okta Domain, e.g. \"dev-35531955.okta.com\"\n\n # Credentials\n # Add secret in Secrets Tab with relevant names for each variable\n okta_api_token: # Your Okta API Token, e.g. \"11be4R_M2MzDqXawbTHfKGpKee0kuEOfX1RCQSRx99\"\n\n # Optional flags to ingest users, groups, or both\n ingest_users: True\n ingest_groups: True\n\n # Optional: Customize the mapping to DataHub Username from an attribute appearing in the Okta User\n # profile. Reference: https://developer.okta.com/docs/reference/api/users/\n # okta_profile_to_username_attr: str = \"login\"\n # okta_profile_to_username_regex: str = \"([^@]+)\"\n \n # Optional: Customize the mapping to DataHub Group from an attribute appearing in the Okta Group\n # profile. Reference: https://developer.okta.com/docs/reference/api/groups/\n # okta_profile_to_group_name_attr: str = \"name\"\n # okta_profile_to_group_name_regex: str = \"(.*)\"\n \n # Optional: Include deprovisioned or suspended Okta users in the ingestion.\n # include_deprovisioned_users = False\n # include_suspended_users = False" }, { "urn": "urn:li:dataPlatform:vertica", diff --git a/datahub-web-react/src/app/ingest/source/utils.ts b/datahub-web-react/src/app/ingest/source/utils.ts index f789ed8434721d..e55d5a598d3d94 100644 --- a/datahub-web-react/src/app/ingest/source/utils.ts +++ b/datahub-web-react/src/app/ingest/source/utils.ts @@ -129,6 +129,20 @@ export const getExecutionRequestStatusDisplayColor = (status: string) => { ); }; +export const validateURL = (fieldName: string) => { + return { + validator(_, value) { + const URLPattern = new RegExp(/^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!$&'()*+,;=.]+$/); + const isURLValid = URLPattern.test(value); + if (!value || isURLValid) { + return Promise.resolve(); + } + return Promise.reject(new Error(`A valid ${fieldName} is required.`)); + }, + }; +}; + + const ENTITIES_WITH_SUBTYPES = new Set([ EntityType.Dataset.toLowerCase(), EntityType.Container.toLowerCase(), diff --git a/docs-website/yarn.lock b/docs-website/yarn.lock index c7ce27dd6431d0..44bc206728532b 100644 --- a/docs-website/yarn.lock +++ b/docs-website/yarn.lock @@ -4901,13 +4901,14 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== -es5-ext@^0.10.35, es5-ext@^0.10.50: - version "0.10.62" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" - integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== +es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@^0.10.62, es5-ext@~0.10.14: + version "0.10.63" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.63.tgz#9c222a63b6a332ac80b1e373b426af723b895bd6" + integrity sha512-hUCZd2Byj/mNKjfP9jXrdVZ62B8KuA/VoK7X8nUh5qT+AxDmcbvZz041oDVZdbIN1qW6XY9VDNwzkvKnZvK2TQ== dependencies: es6-iterator "^2.0.3" es6-symbol "^3.1.3" + esniff "^2.0.1" next-tick "^1.1.0" es6-iterator@^2.0.3: @@ -4965,6 +4966,16 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -5010,6 +5021,14 @@ eval@^0.1.8: "@types/node" "*" require-like ">= 0.1.1" +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -5259,9 +5278,9 @@ flux@^4.0.1: fbjs "^3.0.1" follow-redirects@^1.0.0, follow-redirects@^1.14.7: - version "1.15.4" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" - integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.3" diff --git a/docs/api/tutorials/domains.md b/docs/api/tutorials/domains.md index 617864d233b7a6..800d3dbff5614a 100644 --- a/docs/api/tutorials/domains.md +++ b/docs/api/tutorials/domains.md @@ -79,6 +79,41 @@ You can now see `Marketing` domain has been created under `Govern > Domains`.
+### Creating a Nested Domain + +You can also create a nested domain, or a domain within another domain. + +