diff --git a/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/README.md b/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/README.md index 1cb094e2..4d5e4f77 100644 --- a/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/README.md +++ b/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/README.md @@ -49,6 +49,7 @@ The Azure resources for this solution consist of the following: | kv-schoology-sds | Key vault | Contains the ClientSecretForSdsCsvADF key which should have the client secret created via the App Registration for the ADF instance (details further down) | | stschoologycsvsds | Storage account | V2 storage account, Read-access geo-redundant storage, Encryption type: Microsoft-managed keys | | schoologyimportcsvs | Blob storage | The container where the Schoology import files reside.  SFTP can be enabled on the storage account if transferring source files from outside the Azure tenant. (Note: The name is a suggestion and can be any name.) | +| resources | Blob storage | The container which has files used by this ADF solution. | The setup within the Azure subscription consists of provisioning and configuring the above resources with @@ -65,30 +66,24 @@ following steps: upload.  Create a resource group where to place the resources if not already done. (suggested name rg-SchoologyCSVtoSDS). Note: The optional parameters on the confirmation screen can be filled in later during the ADF setup. -4) Modify the storage account access to enable managed identity adf-SchoologyCSVtoSDS (ADF instance) to read and modify contents. (“Storage Blob Data Contributor” role). +4) Modify the storage account access to enable authorized users to read and modify contents. (“Storage Blob Data Contributor” role). Also, the user’s IP address should only be temporarily added in the firewall in the networking tab before updating the storage contents. This must be done even if the user has access control privileges. [Assign Azure roles using the Azure portal - Azure RBAC | Microsoft Learn](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) -5) Do the same for authorized users who need to modify data in storage. Also, the user’s IP address should only be temporarily added in the firewall in the networking tab before updating the storage contents. This must be done even if the user has access control privileges. - -6) If you want to use SFTP, modify the storage account access to enable managed +5) If you want to use SFTP, modify the storage account access to enable managed identity adf-SchoologyCSVtoSDS (ADF instance) to toggle SFTP. (“Storage Account Contributor” role). The incoming IP address should also be added to firewall in the storage account. -7) Modify key vault access to enable managed identity adf-SchoologyCSVtoSDS (ADF - instance) to retrieve secrets from the key vault (Assign “Key Vault Secrets - User” role). [Grant - permission to applications to access an Azure key vault using Azure RBAC | - Microsoft Learn](https://learn.microsoft.com/en-us/azure/key-vault/general/rbac-guide?tabs=azure-portal) +6) Upload the file in the repo named enumMap.csv in the resources container. This modify the file to include any values that are in your files that are not supported by SDS. The default SDS values are at the following link: [Default list of values - School Data Sync | Microsoft Learn](https://learn.microsoft.com/en-us/schooldatasync/default-list-of-values) -8) Modify the key vault to provide access to users who need to update the secret values. (At least “Key Vault Secrets Officer” role for creating). Also, the user’s IP address should only be temporarily added in the firewall in the networking tab before updating the key vault secrets. This must be done even if the user has access control privileges. +7) Modify key vault access to provide access to users who need to update the secret values. (At least “Key Vault Secrets Officer” role for creating). Also, the user’s IP address should only be temporarily added in the firewall in the networking tab before updating the key vault secrets. This must be done even if the user has access control privileges. -9) Create an app registration in Entra to allow the ADF resource to call the Graph API’s needed then create a secret for the app registration. [Quickstart: +8) Create an app registration in Entra to allow the ADF resource to call the Graph API’s needed then create a secret for the app registration. [Quickstart: Register an app in the Microsoft identity platform - Microsoft identity platform | Microsoft Learn](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) -10) Add the key vault secret values needed from the above table (Existing values were created as dummies and can be disabled). [Azure - Quickstart - Set and retrieve a secret from Key Vault using Azure portal | - Microsoft Learn](https://learn.microsoft.com/en-us/azure/key-vault/secrets/quick-create-portal) +9) Add the key vault secret values needed from the above table (Existing values were created as dummies and can be disabled). [Azure + Quickstart - Set and retrieve a secret from Key Vault using Azure portal | + Microsoft Learn](https://learn.microsoft.com/en-us/azure/key-vault/secrets/quick-create-portal) -11) Add the Graph API application permissions from the table below to the app +10) Add the Graph API application permissions from the table below to the app registration.  Remember to grant admin consent for the added permissions. [Quickstart: Configure an app to access a web API - Microsoft identity platform | Microsoft Learn](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis) @@ -104,15 +99,12 @@ following steps: ## Data Factory setup -1) If not already done, create a container where the Schoology import files for the ADF instance will reside (Suggest naming it "schoologyimportcsvs" within the same storage account in the newly created resource group). - -2) Go to “Private endpoint connections” in the networking tab for both the key vault and storage account and approve each.  (Also verify that public access is disabled - and there are no exceptions in “Firewalls and virtual networks”) +1) Go to “Private endpoint connections” in the networking tab for both the key vault and storage account, and ensure that each is approved.  (Also verify that public access is disabled and there are no exceptions in “Firewalls and virtual networks”) -3) Go to the Data Factory named adf-SchoologyCSVtoSDS in Azure Portal and +2) Go to the Data Factory named adf-SchoologyCSVtoSDS in Azure Portal and click ‘Launch studio’ to make changes. Once inside, go to the Manage tab on the left menu. -4) The final step in the ADF setup is to configure the global parameters in the Manage +3) The final step in the ADF setup is to configure the global parameters in the Manage menu as shown below, and further described in the table following. | **Global parameter name** | **Type** | **Description** | diff --git a/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/enumMap.csv b/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/enumMap.csv new file mode 100644 index 00000000..4c68f05e --- /dev/null +++ b/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/enumMap.csv @@ -0,0 +1,31 @@ +"customValue","sdsValue","type" +"","principal","OrganizationUserRoles" +"","chair","OrganizationUserRoles" +"","professor","OrganizationUserRoles" +"","researcher","OrganizationUserRoles" +"","adjunct","OrganizationUserRoles" +"","affiliate","OrganizationUserRoles" +"","occupationalTherapist","OrganizationUserRoles" +"","physicalTherapist","OrganizationUserRoles" +"","speechTherapist","OrganizationUserRoles" +"","visionTherapist","OrganizationUserRoles" +"","paraprofessional","OrganizationUserRoles" +"","specialServices","OrganizationUserRoles" +"","advisor","OrganizationUserRoles" +"","proctor","OrganizationUserRoles" +"","nurse","OrganizationUserRoles" +"","officeStaff","OrganizationUserRoles" +"","lecturer","OrganizationUserRoles" +"","itAdmin","OrganizationUserRoles" +"","administrator","OrganizationUserRoles" +"teacher","teacher","OrganizationUserRoles" +"","faculty","OrganizationUserRoles" +"","staff","OrganizationUserRoles" +"","teacherAssistant","OrganizationUserRoles" +"","assistant","OrganizationUserRoles" +"","instructor","OrganizationUserRoles" +"","substitute","OrganizationUserRoles" +"","coach","OrganizationUserRoles" +"","alumni","OrganizationUserRoles" +"student","student","OrganizationUserRoles" +"","other","OrganizationUserRoles" diff --git a/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/schoologyCSV-SDS_ADF_ARM_template.json b/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/schoologyCSV-SDS_ADF_ARM_template.json index 766edb85..b08d4ca8 100644 --- a/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/schoologyCSV-SDS_ADF_ARM_template.json +++ b/CustomSolutions/SDS_ADF_ETL_Integrations/SchoologyCSV/schoologyCSV-SDS_ADF_ARM_template.json @@ -10,6 +10,11 @@ "defaultValue": "stschoologycsvsds", "type": "String" }, + "blobContainer_schoologyimportcsvs_name": { + "defaultValue": "schoologyimportcsvs", + "metadata": "Blob container for Schoology CSVs", + "type": "String" + }, "factoryName": { "type": "string", "metadata": "Data Factory name", @@ -133,7 +138,11 @@ "stschoologycsvsds_properties_privateLinkResourceId":"[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Storage/storageAccounts/', parameters('storageAccounts_stschoologycsvsds_name'))]", "stschoologycsvsds_properties_groupId":"dfs", "stschoologycsvsds_properties_fqdns":"[concat(parameters('storageAccounts_stschoologycsvsds_name'), '.dfs.core.windows.net')]", - "factoryId": "[concat('Microsoft.DataFactory/factories/', parameters('factoryName'))]" + "kv-adf_roleAssignmentId":"[guid(concat(resourceGroup().id, 'Key Vault Secrets User'))]", + "st-adf_roleAssignmentId":"[guid(concat(resourceGroup().id, 'Storage Blob Data Contributor'))]", + "factoryId": "[concat('Microsoft.DataFactory/factories/', parameters('factoryName'))]", + "factoryName": "[parameters('factoryName')]", + "stName": "[parameters('storageAccounts_stschoologycsvsds_name')]" }, "resources": [ { @@ -203,6 +212,22 @@ "accessTier": "Hot" } }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccounts_stschoologycsvsds_name'), 'default', parameters('blobContainer_schoologyimportcsvs_name'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('storageAccounts_stschoologycsvsds_name'), 'default')]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccounts_stschoologycsvsds_name'), 'default', 'resources')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('storageAccounts_stschoologycsvsds_name'), 'default')]" + ] + }, { "type": "Microsoft.KeyVault/vaults/secrets", "apiVersion": "2023-07-01", @@ -294,6 +319,36 @@ } } }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[variables('st-adf_roleAssignmentId')]", + "scope": "[concat('Microsoft.Storage/storageAccounts', '/', parameters('storageAccounts_stschoologycsvsds_name'))]", + "properties": { + "roleDefinitionId": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalId": "[reference(concat('Microsoft.DataFactory/factories', '/', parameters('factoryName')), '2018-06-01', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccounts_stschoologycsvsds_name'))]", + "[resourceId('Microsoft.DataFactory/factories', parameters('factoryName'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[variables('kv-adf_roleAssignmentId')]", + "scope": "[concat('Microsoft.KeyVault/vaults', '/', parameters('vaults_kv_schoology_sds_name'))]", + "properties": { + "roleDefinitionId": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/4633458b-17de-408a-b874-0445c86b69e6')]", + "principalId": "[reference(concat('Microsoft.DataFactory/factories', '/', parameters('factoryName')), '2018-06-01', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', parameters('vaults_kv_schoology_sds_name'))]", + "[resourceId('Microsoft.DataFactory/factories', parameters('factoryName'))]" + ] + }, { "name": "[concat(parameters('factoryName'), '/default')]", "type": "Microsoft.DataFactory/factories/managedVirtualNetworks", @@ -301,7 +356,7 @@ "properties": {}, "dependsOn": [ "[resourceId('Microsoft.DataFactory/factories', parameters('factoryName'))]" - ] + ] }, { "name": "[concat(parameters('factoryName'), '/AutoResolveIntegrationRuntime')]", @@ -994,6 +1049,47 @@ "[concat(variables('factoryId'), '/linkedServices/stsdsvnext')]" ] }, + { + "name": "[concat(parameters('factoryName'), '/SDS_Enum_Map_CSV')]", + "type": "Microsoft.DataFactory/factories/datasets", + "apiVersion": "2018-06-01", + "properties": { + "linkedServiceName": { + "referenceName": "stsdsvnext", + "type": "LinkedServiceReference" + }, + "annotations": [], + "type": "DelimitedText", + "typeProperties": { + "location": { + "type": "AzureBlobFSLocation", + "fileName": "enumMap.csv", + "fileSystem": "resources" + }, + "columnDelimiter": ",", + "escapeChar": "\\", + "firstRowAsHeader": true, + "quoteChar": "\"" + }, + "schema": [ + { + "name": "customValue", + "type": "String" + }, + { + "name": "sdsValue", + "type": "String" + }, + { + "name": "type", + "type": "String" + } + ] + }, + "dependsOn": [ + "[concat(variables('factoryId'), '/linkedServices/stsdsvnext')]" + ] + }, { "name": "[concat(parameters('factoryName'), '/SDSVnext_Demographics')]", "type": "Microsoft.DataFactory/factories/datasets", @@ -6085,6 +6181,7 @@ "type": "Expression" } }, + "enumMap": {}, "SDSVnextRoles": { "sdsStorageContainer": "@pipeline().parameters.sdsStorageContainer", "sdsStorageFolder": { @@ -6411,7 +6508,7 @@ " allowSchemaDrift: true,", " validateSchema: false,", " ignoreNoFilesFound: false) ~> SchoologyEnrollments", - "selectRequiredFields derive(classSouredId = replace(classSourcedId, ' ', '_'),", + "selectRequiredFields derive(classSouredId = trim(classSourcedId),", " role = lower(role)) ~> transformFields", "SchoologyEnrollments select(mapColumn(", " classSourcedId = {Section School Code},", @@ -8461,6 +8558,13 @@ "type": "DatasetReference" }, "name": "SchoologyUsers" + }, + { + "dataset": { + "referenceName": "SDS_Enum_Map_CSV", + "type": "DatasetReference" + }, + "name": "enumMap" } ], "sinks": [ @@ -8481,7 +8585,10 @@ "name": "selectRequiredColumns" }, { - "name": "lowerCaseRole" + "name": "lookupOrgUserRoles" + }, + { + "name": "filterOrgUserRoles" } ], "scriptLines": [ @@ -8499,6 +8606,14 @@ " allowSchemaDrift: true,", " validateSchema: false,", " ignoreNoFilesFound: false) ~> SchoologyUsers", + "source(output(", + " customValue as string,", + " sdsValue as string,", + " type as string", + " ),", + " allowSchemaDrift: true,", + " validateSchema: false,", + " ignoreNoFilesFound: false) ~> enumMap", "SchoologyUsers select(mapColumn(", " userSourcedId = {Unique User ID},", " orgSourcedId = School,", @@ -8507,8 +8622,11 @@ " ),", " skipDuplicateMapInputs: true,", " skipDuplicateMapOutputs: true) ~> selectRequiredColumns", - "selectRequiredColumns derive(role = lower(role)) ~> lowerCaseRole", - "lowerCaseRole sink(allowSchemaDrift: true,", + "selectRequiredColumns, filterOrgUserRoles lookup(role == customValue,", + " multiple: true,", + " broadcast: 'both')~> lookupOrgUserRoles", + "enumMap filter(equals(type, 'OrganizationUserRoles')) ~> filterOrgUserRoles", + "lookupOrgUserRoles sink(allowSchemaDrift: true,", " validateSchema: false,", " input(", " userSourcedId as string,", @@ -8529,7 +8647,7 @@ " mapColumn(", " userSourcedId,", " orgSourcedId,", - " role,", + " role = sdsValue,", " grade", " ),", " partitionBy('hash', 1)) ~> SDSVnextRoles" @@ -8538,6 +8656,7 @@ }, "dependsOn": [ "[concat(variables('factoryId'), '/datasets/Schoology_Users_CSV')]", + "[concat(variables('factoryId'), '/datasets/SDS_Enum_Map_CSV')]", "[concat(variables('factoryId'), '/datasets/SDSVnext_Roles')]", "[concat(variables('factoryId'), '/linkedServices/stsdsvnext')]" ] @@ -9223,7 +9342,7 @@ " ),", " skipDuplicateMapInputs: true,", " skipDuplicateMapOutputs: true) ~> selectRequiredColumns", - "selectRequiredColumns derive(sourcedId = replace(sourcedId, ' ', '_')) ~> derivedSourcedId", + "selectRequiredColumns derive(sourcedId = trim(sourcedId) ~> derivedSourcedId", "derivedSourcedId sink(allowSchemaDrift: true,", " validateSchema: false,", " input(", @@ -9381,6 +9500,75 @@ "principalId": "", "tenantId": "" } + }, + { + "type": "Microsoft.KeyVault/vaults/privateEndpointConnections", + "apiVersion": "2024-11-01", + "name": "[concat(parameters('vaults_kv_schoology_sds_name') ,'/', parameters('factoryName') ,'.', parameters('vaults_kv_schoology_sds_name'), '-conn')]", + "properties": { + "privateEndpoint": {}, + "privateLinkServiceConnectionState": { + "status": "Approved", + "description": "ARM - AutoApproved", + "actionRequired": "None" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/managedVirtualNetworks/managedPrivateEndpoints', parameters('factoryName'), 'default', parameters('vaults_kv_schoology_sds_name'))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "[concat(deployment().name, '_ADF_MPE_ST_Approve')]", + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/managedVirtualNetworks/managedPrivateEndpoints', variables('factoryName'), 'default', variables('stName'))]" + ], + "properties": { + "mode": "Incremental", + "expressionEvaluationOptions": { + "scope": "inner" + }, + "parameters": { + "ST-MPEs-ToApprove": { + "value": "[filter(reference(resourceId('Microsoft.Storage/storageAccounts', variables('stName')), '2023-04-01').privateEndpointConnections, lambda('pe', not(equals(lambdaVariables('pe').properties.privateLinkServiceConnectionState.status, 'Approved'))))]" + }, + "stName": { + "value": "[variables('stName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "ST-MPEs-ToApprove": { + "type": "array" + }, + "stName": { + "type": "String" + } + }, + "resources": [ + { + "copy": { + "name": "Copy_ST-PE-Approval", + "count": "[length(parameters('ST-MPEs-ToApprove'))]" + }, + "type": "Microsoft.Storage/storageAccounts/privateEndpointConnections", + "apiVersion": "2023-05-01", + "name": "[concat(parameters('stName') ,'/', parameters('ST-MPEs-ToApprove')[copyIndex('Copy_ST-PE-Approval')].name)]", + "properties": { + "privateEndpoint": {}, + "privateLinkServiceConnectionState": { + "status": "Approved", + "description": "ARM - AutoApproved", + "actionRequired": "None" + } + } + } + ] + } + } } ] }