Skip to content

Commit 99deac9

Browse files
Add files via upload
1 parent f49d927 commit 99deac9

File tree

2 files changed

+334
-0
lines changed

2 files changed

+334
-0
lines changed
+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<#
2+
.SYNOPSIS
3+
Given a new [file].bacpac, creates a new [file]-patched.bacpac with master key information removed from the model.xml and origin.xml.
4+
.DESCRIPTION
5+
When exporting a .bacpac from Azure SQLDB with auditing enabled, the .bacpac will contain a master key without a password in the model.xml. A
6+
master key without a password is an Azure SQLDB only feature, so it's presence prevents being able to import the .bacpac into an on-premise
7+
SQL Server database. This script works around this limitation by extracting the model.xml and origin.xml from the .bacpac, removing the references
8+
to the master key, and creating a new .bacpac with the updated model.xml and origin.xml. The resulting .bacpac can then be imported to an on-premise
9+
database.
10+
.EXAMPLE
11+
C:\PS> .\RemoveMasterKey.ps1 -bacpacPath "C:\BacPacs\Test.bacpac" # Generates a Test-patched.bacpac file
12+
.PARAMETER bacpacPath
13+
Specifies the path the .bacpac to patch.
14+
#>
15+
param(
16+
[Parameter(Mandatory=$true, HelpMessage="Specifies the path the .bacpac. This file will not be modified.")]
17+
[string]$bacpacPath
18+
)
19+
20+
21+
if ($PSVersionTable.PSVersion.Major -lt 4) {
22+
Write-Host "Unsupported powershell version. This script requires powershell version 4.0 or later"
23+
return
24+
}
25+
26+
27+
Add-Type -Assembly System.IO.Compression.FileSystem
28+
29+
30+
$targetBacpacPath = [System.IO.Path]::Combine(
31+
[System.IO.Path]::GetDirectoryName($bacpacPath),
32+
[System.IO.Path]::GetFileNameWithoutExtension($bacpacPath) + "-patched" + [System.IO.Path]::GetExtension($bacpacPath))
33+
$originXmlFile = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($targetBacpacPath), "Origin.xml")
34+
$modelXmlFile = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($targetBacpacPath), "model.xml")
35+
36+
37+
if ([System.IO.File]::Exists($targetBacpacPath)) {
38+
[System.IO.File]::Delete($targetBacpacPath)
39+
}
40+
41+
42+
#
43+
# Extract the model.xml and Origin.xml from the .bacpac
44+
#
45+
$zip = [System.IO.Compression.ZipFile]::OpenRead($bacpacPath)
46+
foreach ($entry in $zip.Entries) {
47+
if ([string]::Compare($entry.Name, "model.xml", $True) -eq 0) {
48+
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $modelXmlFile, $true)
49+
break
50+
}
51+
}
52+
foreach ($entry in $zip.Entries) {
53+
if ([string]::Compare($entry.Name, "Origin.xml", $True) -eq 0) {
54+
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $originXmlFile, $true)
55+
break
56+
}
57+
}
58+
$zip.Dispose()
59+
60+
61+
if(![System.IO.File]::Exists($modelXmlFile)) {
62+
Write-Host "Could not extract the model.xml from file " + $bacpacPath
63+
return
64+
}
65+
if(![System.IO.File]::Exists($originXmlFile)) {
66+
Write-Host "Could not extract the Origin.xml from file " + $bacpacPath
67+
return
68+
}
69+
70+
71+
#
72+
# Modify the model.xml
73+
#
74+
[xml]$modelXml = Get-Content $modelXmlFile
75+
$ns = New-Object System.Xml.XmlNamespaceManager($modelXml.NameTable)
76+
$ns.AddNamespace("x", $modelXml.DocumentElement.NamespaceURI)
77+
78+
79+
$masterKeyNodes = $modelXml.SelectNodes("//x:DataSchemaModel/x:Model/x:Element[@Type='SqlMasterKey']", $ns)
80+
foreach ($masterKeyNode in $masterKeyNodes) {
81+
$masterKeyNode.ParentNode.RemoveChild($masterKeyNode)
82+
}
83+
84+
85+
$sqlDatabaseCredentialNodes = $modelXml.SelectNodes("//x:DataSchemaModel/x:Model/x:Element[@Type='SqlDatabaseCredential']", $ns)
86+
foreach ($sqlDatabaseCredentialNode in $sqlDatabaseCredentialNodes) {
87+
if ($sqlDatabaseCredentialNode.Property.Name -eq "Identity" -and $sqlDatabaseCredentialNode.Property.Value -eq "SHARED ACCESS SIGNATURE")
88+
{
89+
$sqlDatabaseCredentialNode.ParentNode.RemoveChild($sqlDatabaseCredentialNode)
90+
}
91+
}
92+
93+
94+
$modelXml.Save($modelXmlFile)
95+
96+
97+
#
98+
# Modify the Origin.xml
99+
#
100+
[xml]$originXml = Get-Content $originXmlFile
101+
$ns = New-Object System.Xml.XmlNamespaceManager($originXml.NameTable)
102+
$ns.AddNamespace("x", $originXml.DocumentElement.NamespaceURI)
103+
104+
105+
$databaseCredentialNode = $originXml.SelectSingleNode("//x:DacOrigin/x:Server/x:ObjectCounts/x:DatabaseCredential", $ns)
106+
if ($databaseCredentialNode) {
107+
if ($databaseCredentialNode.InnerText -eq "1") {
108+
$databaseCredentialNode.ParentNode.RemoveChild($databaseCredentialNode)
109+
} else {
110+
$databaseCredentialNode.InnerText = $databaseCredentialNode.Value - 1
111+
}
112+
}
113+
114+
115+
$masterKeyNode = $originXml.SelectSingleNode("//x:DacOrigin/x:Server/x:ObjectCounts/x:MasterKey", $ns)
116+
if ($masterKeyNode) {
117+
$masterKeyNode.ParentNode.RemoveChild($masterKeyNode)
118+
}
119+
120+
121+
$modelXmlHash = (Get-FileHash $modelXmlFile -Algorithm SHA256).Hash
122+
$checksumNode = $originXml.SelectSingleNode("//x:DacOrigin/x:Checksums/x:Checksum", $ns)
123+
if ($checksumNode) {
124+
$checksumNode.InnerText = $modelXmlHash
125+
}
126+
127+
128+
$originXml.Save($originXmlFile)
129+
130+
131+
#
132+
# Create the new .bacpac using the patched model.xml and Origin.xml
133+
#
134+
$zipSource = [System.IO.Compression.ZipFile]::OpenRead($bacpacPath)
135+
$zipTarget = [System.IO.Compression.ZipFile]::Open($targetBacpacPath, "Create")
136+
foreach ($entry in $zipSource.Entries) {
137+
if ([string]::Compare($entry.Name, "Origin.xml", $True) -eq 0) {
138+
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipTarget, $originXmlFile, $entry.FullName)
139+
} elseif ([string]::Compare($entry.Name, "model.xml", $True) -eq 0) {
140+
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipTarget, $modelXmlFile, $entry.FullName)
141+
} else {
142+
$targetEntry = $zipTarget.CreateEntry($entry.FullName)
143+
$sourceStream = $null
144+
$targetStream = $null
145+
try {
146+
$sourceStream = [System.IO.Stream]$entry.Open()
147+
$targetStream = [System.IO.Stream]$targetEntry.Open()
148+
$sourceStream.CopyTo($targetStream)
149+
}
150+
finally {
151+
if ($targetStream -ne $null) {
152+
$targetStream.Dispose()
153+
}
154+
if ($sourceStream -ne $null) {
155+
$sourceStream.Dispose()
156+
}
157+
}
158+
}
159+
}
160+
$zipSource.Dispose()
161+
$zipTarget.Dispose()
162+
163+
164+
[System.IO.File]::Delete($modelXmlFile)
165+
[System.IO.File]::Delete($originXmlFile)
166+
167+
168+
Write-Host "Completed update to the model.xml and Origin.xml in file"([System.IO.Path]::GetFullPath($targetBacpacPath))
+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<#
2+
.SYNOPSIS
3+
Patches the model.xml and origin.xml in a .bacpac with the master key information removed.
4+
.DESCRIPTION
5+
When exporting a .bacpac from Azure SQLDB with auditing enabled, the .bacpac will contain a master key without a password in the model.xml. A
6+
master key without a password is an Azure SQLDB only feature, so it's presence prevents being able to import the .bacpac into an on-premise
7+
SQL Server database. This script works around this limitation by extracting the model.xml and origin.xml from the .bacpac, removing the references
8+
to the master key, and then updating the .bacpac with the new model.xml and origin.xml. The resulting .bacpac can then be imported to an on-premise
9+
database. By default, a copy of the original .bacpac is made, and the copy is updated. Using the -skipCopy parameter will skip the copy step.
10+
.EXAMPLE
11+
C:\PS> .\RemoveMasterKey.ps1 -bacpacPath "C:\BacPacs\Test.bacpac" -skipCopy
12+
.PARAMETER bacpacPath
13+
Specifies the path the .bacpac to patch.
14+
.PARAMETER skipCopy
15+
If specified, copies the .bacpac before making updates.
16+
#>
17+
param(
18+
[Parameter(Mandatory=$true, HelpMessage="Specifies the path the .bacpac to patch.")]
19+
[string]$bacpacPath,
20+
[Parameter(Mandatory=$false, HelpMessage="If specified, copies the .bacpac before making updates.")]
21+
[switch]$skipCopy
22+
)
23+
24+
25+
if ($PSVersionTable.PSVersion.Major -lt 4)
26+
{
27+
Write-Host "Unsupported powershell version. This script requires powershell version 4.0 or later"
28+
return
29+
}
30+
31+
32+
Add-Type -Assembly System.IO.Compression.FileSystem
33+
34+
35+
$targetBacpacPath = [System.IO.Path]::GetFullPath($bacpacPath)
36+
if (!$skipCopy)
37+
{
38+
$targetBacpacPath = [System.IO.Path]::Combine(
39+
[System.IO.Path]::GetDirectoryName($targetBacpacPath),
40+
[System.IO.Path]::GetFileNameWithoutExtension($targetBacpacPath) + "-patched" + [System.IO.Path]::GetExtension($targetBacpacPath))
41+
42+
Copy-Item $bacpacPath $targetBacpacPath
43+
}
44+
45+
46+
$originXmlFile = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($targetBacpacPath), "Origin.xml")
47+
$modelXmlFile = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($targetBacpacPath), "model.xml")
48+
49+
50+
#
51+
# Extract the model.xml and Origin.xml from the .bacpac
52+
#
53+
$zip = [IO.Compression.ZipFile]::OpenRead($targetBacpacPath)
54+
foreach ($entry in $zip.Entries) {
55+
if ([string]::Compare($entry.Name, "model.xml", $True) -eq 0) {
56+
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $modelXmlFile, $true)
57+
break
58+
}
59+
}
60+
foreach ($entry in $zip.Entries) {
61+
if ([string]::Compare($entry.Name, "Origin.xml", $True) -eq 0) {
62+
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $originXmlFile, $true)
63+
break
64+
}
65+
}
66+
$zip.Dispose()
67+
68+
69+
if(![System.IO.File]::Exists($modelXmlFile)) {
70+
Write-Host "Could not extract the model.xml from file " + $targetBacpacPath
71+
return
72+
}
73+
if(![System.IO.File]::Exists($originXmlFile)) {
74+
Write-Host "Could not extract the Origin.xml from file " + $targetBacpacPath
75+
return
76+
}
77+
78+
79+
#
80+
# Modify the model.xml
81+
#
82+
[xml]$modelXml = Get-Content $modelXmlFile
83+
$ns = New-Object System.Xml.XmlNamespaceManager($modelXml.NameTable)
84+
$ns.AddNamespace("x", $modelXml.DocumentElement.NamespaceURI)
85+
86+
87+
$masterKeyNodes = $modelXml.SelectNodes("//x:DataSchemaModel/x:Model/x:Element[@Type='SqlMasterKey']", $ns)
88+
foreach ($masterKeyNode in $masterKeyNodes) {
89+
$masterKeyNode.ParentNode.RemoveChild($masterKeyNode)
90+
}
91+
92+
93+
$sqlDatabaseCredentialNodes = $modelXml.SelectNodes("//x:DataSchemaModel/x:Model/x:Element[@Type='SqlDatabaseCredential']", $ns)
94+
foreach ($sqlDatabaseCredentialNode in $sqlDatabaseCredentialNodes) {
95+
if ($sqlDatabaseCredentialNode.Property.Name -eq "Identity" -and $sqlDatabaseCredentialNode.Property.Value -eq "SHARED ACCESS SIGNATURE")
96+
{
97+
$sqlDatabaseCredentialNode.ParentNode.RemoveChild($sqlDatabaseCredentialNode)
98+
}
99+
}
100+
101+
102+
$modelXml.Save($modelXmlFile)
103+
104+
105+
#
106+
# Modify the Origin.xml
107+
#
108+
[xml]$originXml = Get-Content $originXmlFile
109+
$ns = New-Object System.Xml.XmlNamespaceManager($originXml.NameTable)
110+
$ns.AddNamespace("x", $originXml.DocumentElement.NamespaceURI)
111+
112+
113+
$databaseCredentialNode = $originXml.SelectSingleNode("//x:DacOrigin/x:Server/x:ObjectCounts/x:DatabaseCredential", $ns)
114+
if ($databaseCredentialNode) {
115+
if ($databaseCredentialNode.InnerText -eq "1") {
116+
$databaseCredentialNode.ParentNode.RemoveChild($databaseCredentialNode)
117+
} else {
118+
$databaseCredentialNode.InnerText = $databaseCredentialNode.Value - 1
119+
}
120+
}
121+
122+
123+
$masterKeyNode = $originXml.SelectSingleNode("//x:DacOrigin/x:Server/x:ObjectCounts/x:MasterKey", $ns)
124+
if ($masterKeyNode) {
125+
$masterKeyNode.ParentNode.RemoveChild($masterKeyNode)
126+
}
127+
128+
129+
$modelXmlHash = (Get-FileHash $modelXmlFile -Algorithm SHA256).Hash
130+
$checksumNode = $originXml.SelectSingleNode("//x:DacOrigin/x:Checksums/x:Checksum", $ns)
131+
if ($checksumNode) {
132+
$checksumNode.InnerText = $modelXmlHash
133+
}
134+
135+
136+
$originXml.Save($originXmlFile)
137+
138+
139+
#
140+
# Update the model.xml and Origin.xml in the .bacpac
141+
#
142+
$zip = [System.IO.Compression.ZipFile]::Open($targetBacpacPath, "Update")
143+
foreach ($entry in $zip.Entries) {
144+
if ([string]::Compare($entry.Name, "Origin.xml", $True) -eq 0) {
145+
$entryName = $entry.FullName
146+
$entry.Delete()
147+
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $originXmlFile, $entryName)
148+
break
149+
}
150+
}
151+
foreach ($entry in $zip.Entries) {
152+
if ([string]::Compare($entry.Name, "model.xml", $True) -eq 0) {
153+
$entryName = $entry.FullName
154+
$entry.Delete()
155+
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $modelXmlFile, $entryName)
156+
break
157+
}
158+
}
159+
$zip.Dispose()
160+
161+
162+
[System.IO.File]::Delete($modelXmlFile)
163+
[System.IO.File]::Delete($originXmlFile)
164+
165+
166+
Write-Host "Completed update to the model.xml and Origin.xml in file "([System.IO.Path]::GetFullPath($targetBacpacPath))

0 commit comments

Comments
 (0)