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

Fix extension columns obfuscation #15

Merged
merged 14 commits into from
Mar 4, 2024
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
Binary file removed assets/ObfuscatorTest.pbix
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -35,36 +35,43 @@ internal string DeobfuscateExpression(string expression)
case DaxToken.DELIMITED_COMMENT:
tokenText = _dictionary.GetValue(tokenText);
break;
case DaxToken.COLUMN_OR_MEASURE when token.IsReservedExtensionColumn():
case DaxToken.COLUMN_OR_MEASURE when token.IsReservedTokenName():
tokenText = token.Replace(expression, tokenText);
break;
case DaxToken.STRING_LITERAL when token.IsExtensionColumnName():
tokenText = ReplaceExtensionColumnName(token);
break;
case DaxToken.TABLE_OR_VARIABLE when token.IsVariable():
case DaxToken.TABLE:
case DaxToken.COLUMN_OR_MEASURE:
case DaxToken.STRING_LITERAL:
case DaxToken.UNTERMINATED_COLREF:
case DaxToken.UNTERMINATED_TABLEREF:
case DaxToken.UNTERMINATED_STRING:
tokenText = token.Replace(expression, _dictionary.GetValue(tokenText));
{
if (token.Text.IsFullyQualifiedColumnName())
{
var value = DeobfuscateFullyQualifiedColumnName(tokenText).EscapeDax(token.Type);
tokenText = token.Replace(expression, value);
}
else
{
var value = _dictionary.GetValue(tokenText).EscapeDax(token.Type);
tokenText = token.Replace(expression, value);
}
}
break;
}

builder.Append(tokenText);
}

return builder.ToString();
}

string ReplaceExtensionColumnName(DaxToken token)
{
var (tableName, columnName) = token.GetExtensionColumnNameParts();
tableName = _dictionary.GetValue(tableName);
columnName = _dictionary.GetValue(columnName);
internal string DeobfuscateFullyQualifiedColumnName(string value)
{
var (table, column) = value.GetFullyQualifiedColumnNameParts();
var tableName = _dictionary.GetValue(table);
var columnName = _dictionary.GetValue(column);

var value = $"{tableName.DaxEscape()}[{columnName.DaxEscape()}]";
return token.Replace(expression, value, escape: true);
}
return $"{tableName}[{columnName}]";
}
}
12 changes: 10 additions & 2 deletions src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,16 @@ private void Deobfuscate(DaxName name)
{
if (string.IsNullOrWhiteSpace(name?.Name)) return;

var value = _dictionary.GetValue(name!.Name);
name.Name = value;
if (name!.Name.IsFullyQualifiedColumnName())
{
var value = DeobfuscateFullyQualifiedColumnName(name!.Name);
name.Name = value;
}
else
{
var value = _dictionary.GetValue(name!.Name);
name.Name = value;
}
}

private void Deobfuscate(DaxNote note)
Expand Down
37 changes: 22 additions & 15 deletions src/Dax.Vpax.Obfuscator/DaxModelObfuscator.ObfuscateExpression.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Dax.Vpax.Obfuscator.Extensions;
using System.Text;
using System.Text;
using Dax.Tokenizer;
using Dax.Vpax.Obfuscator.Extensions;

namespace Dax.Vpax.Obfuscator;

Expand Down Expand Up @@ -35,36 +35,43 @@ internal string ObfuscateExpression(string expression)
case DaxToken.DELIMITED_COMMENT:
tokenText = ObfuscateText(new DaxText(tokenText)).ObfuscatedValue;
break;
case DaxToken.COLUMN_OR_MEASURE when token.IsReservedExtensionColumn():
case DaxToken.COLUMN_OR_MEASURE when token.IsReservedTokenName():
tokenText = token.Replace(expression, tokenText);
break;
case DaxToken.STRING_LITERAL when token.IsExtensionColumnName():
tokenText = ReplaceExtensionColumnName(token);
break;
case DaxToken.TABLE_OR_VARIABLE when token.IsVariable():
case DaxToken.TABLE:
case DaxToken.COLUMN_OR_MEASURE:
case DaxToken.STRING_LITERAL:
case DaxToken.UNTERMINATED_COLREF:
case DaxToken.UNTERMINATED_TABLEREF:
case DaxToken.UNTERMINATED_STRING:
tokenText = token.Replace(expression, ObfuscateText(new DaxText(tokenText)));
{
if (token.Text.IsFullyQualifiedColumnName())
{
var value = ObfuscateFullyQualifiedColumnName(tokenText).EscapeDax(token.Type);
tokenText = token.Replace(expression, value);
}
else
{
var value = ObfuscateText(new DaxText(tokenText)).ObfuscatedValue.EscapeDax(token.Type);
tokenText = token.Replace(expression, value);
}
}
break;
}

builder.Append(tokenText);
}

return builder.ToString();
}

string ReplaceExtensionColumnName(DaxToken token)
{
var (tableName, columnName) = token.GetExtensionColumnNameParts();
var tableText = ObfuscateText(new DaxText(tableName));
var columnText = ObfuscateText(new DaxText(columnName));
internal string ObfuscateFullyQualifiedColumnName(string value)
{
var (table, column) = value.GetFullyQualifiedColumnNameParts(obfuscating: true);
var tableName = ObfuscateText(new DaxText(table)).ObfuscatedValue;
var columnName = ObfuscateText(new DaxText(column)).ObfuscatedValue;

var value = $"{tableText.ObfuscatedValue.DaxEscape()}[{columnText.ObfuscatedValue.DaxEscape()}]";
return token.Replace(expression, value, escape: true);
}
return $"{tableName}[{columnName}]";
}
}
24 changes: 17 additions & 7 deletions src/Dax.Vpax.Obfuscator/DaxModelObfuscator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ private void ObfuscateIdentifiers(Column column)

private void ObfuscateIdentifiers(Measure measure)
{
var measureText = Obfuscate(measure.MeasureName) ?? throw new InvalidOperationException($"The measure name is not valid [{measure.MeasureName}].");
var name = measure.MeasureName.Name;
var obfuscatedName = Obfuscate(measure.MeasureName) ?? throw new InvalidOperationException($"The measure name is not valid [{name}].");
CreateKpiMeasure(measure.KpiTargetExpression, "Goal");
CreateKpiMeasure(measure.KpiStatusExpression, "Status");
CreateKpiMeasure(measure.KpiTrendExpression, "Trend");
Expand All @@ -64,8 +65,8 @@ void CreateKpiMeasure(DaxExpression kpi, string type)
{
if (string.IsNullOrWhiteSpace(kpi?.Expression)) return;

var text = new DaxText($"_{measureText.Value} {type}");
text.ObfuscatedValue = $"_{measureText.ObfuscatedValue} {type}";
var text = new DaxText($"_{name} {type}");
text.ObfuscatedValue = $"_{obfuscatedName} {type}";

// It may already exist in case of incremental obfuscation
if (Texts.IsIncrementalObfuscation && Texts.Contains(text))
Expand Down Expand Up @@ -151,13 +152,22 @@ private void Obfuscate(TablePermission tablePermission)
Obfuscate(tablePermission.FilterExpression);
}

private DaxText? Obfuscate(DaxName name)
private string? Obfuscate(DaxName name)
{
if (string.IsNullOrWhiteSpace(name?.Name)) return null;

var text = ObfuscateText(new DaxText(name!.Name));
name.Name = text.ObfuscatedValue;
return text;
if (name!.Name.IsFullyQualifiedColumnName())
{
var value = ObfuscateFullyQualifiedColumnName(name!.Name);
name.Name = value;
}
else
{
var text = ObfuscateText(new DaxText(name!.Name));
name.Name = text.ObfuscatedValue;
}

return name.Name;
}

private void Obfuscate(DaxNote note)
Expand Down
8 changes: 4 additions & 4 deletions src/Dax.Vpax.Obfuscator/DaxTextObfuscator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,11 @@ private static bool IsReservedChar(char @char)
{
// Reserved characters are preserved during obfuscation

switch (@char) {
case '-': // single-line comment char
switch (@char)
{
case ReservedChar_Minus: // single-line comment char
case '/': // multi-line comment char
case '*': // multi-line comment char
case ']': // square bracket escape char e.g. Sales[Rate[%]]]
case '"': // quotation mark escape char e.g. VAR __quotationMarkChar = """"
case '\n': // line feed char e.g. in multi-line comments
case '\r': // carriage return char e.g. in multi-line comments
return true;
Expand All @@ -79,6 +78,7 @@ private static bool IsReservedChar(char @char)
return false;
}

internal const char ReservedChar_Minus = '-';
/// <summary>
/// CALENDAR() [Date] extension column.
/// </summary>
Expand Down
45 changes: 19 additions & 26 deletions src/Dax.Vpax.Obfuscator/Extensions/DaxTokenExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ namespace Dax.Vpax.Obfuscator.Extensions;

internal static class DaxTokenExtensions
{
public static bool IsExtensionColumnName(this DaxToken token)
=> token.Type == DaxToken.STRING_LITERAL && token.Text.EndsWith("]") && token.Text.IndexOf('[') > 0;

public static bool IsVariable(this DaxToken token)
=> token.Type == DaxToken.TABLE_OR_VARIABLE && !IsFunction(token);
{
Debug.Assert(token.Type == DaxToken.TABLE_OR_VARIABLE);
return token.Type == DaxToken.TABLE_OR_VARIABLE && !IsFunction(token);
}

public static bool IsFunction(this DaxToken token)
{
if (token.Type != DaxToken.TABLE_OR_VARIABLE) return false;
Debug.Assert(token.Type == DaxToken.TABLE_OR_VARIABLE);

var current = token.Next;
while (current != null && current.CommentOrWhitespace)
Expand All @@ -22,9 +22,9 @@ public static bool IsFunction(this DaxToken token)
return current != null && current.Type == DaxToken.OPEN_PARENS;
}

public static bool IsReservedExtensionColumn(this DaxToken token)
public static bool IsReservedTokenName(this DaxToken token)
{
if (token.Type != DaxToken.COLUMN_OR_MEASURE) return false;
Debug.Assert(token.Type == DaxToken.COLUMN_OR_MEASURE);

if (token.Text.StartsWith(DaxTextObfuscator.ReservedToken_Value, StringComparison.OrdinalIgnoreCase))
{
Expand All @@ -44,28 +44,21 @@ public static bool IsReservedExtensionColumn(this DaxToken token)
return false;
}

public static (string tableName, string columnName) GetExtensionColumnNameParts(this DaxToken token)
{
Debug.Assert(token.IsExtensionColumnName());

var openIndex = token.Text.IndexOf('[');
var closeIndex = token.Text.LastIndexOf(']');
var tableName = token.Text.Substring(0, openIndex);
var columnName = token.Text.Substring(openIndex + 1, closeIndex - openIndex - 1);
return (tableName, columnName);
}

public static string Replace(this DaxToken token, string expression, DaxText text)
=> Replace(token, expression, text.ObfuscatedValue);

public static string Replace(this DaxToken token, string expression, string value, bool escape = false)
public static string Replace(this DaxToken token, string expression, string value)
{
var substring = expression.Substring(token.StartIndex, token.StopIndex - token.StartIndex + 1);
var tokenText = escape ? token.Text.DaxEscape() : token.Text;

if (substring.IndexOf(tokenText, StringComparison.Ordinal) == -1)
throw new InvalidOperationException($"Failed to replace token >> {token.Type} | {substring} | {tokenText} | {value}");
switch (token.Type)
{
case DaxToken.TABLE:
case DaxToken.STRING_LITERAL:
case DaxToken.COLUMN_OR_MEASURE:
return string.Concat(substring[0], value, substring[substring.Length - 1]);
case DaxToken.UNTERMINATED_TABLEREF:
case DaxToken.UNTERMINATED_COLREF:
return string.Concat(substring[0], value);
}

return substring.Replace(tokenText, value);
return substring.Replace(token.Text, value);
}
}
69 changes: 66 additions & 3 deletions src/Dax.Vpax.Obfuscator/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,70 @@
namespace Dax.Vpax.Obfuscator.Extensions;
using System.Diagnostics;
using Dax.Tokenizer;

namespace Dax.Vpax.Obfuscator.Extensions;

internal static class StringExtensions
{
public static string DaxEscape(this string value)
=> value.Replace("\"", "\"\"").Replace("'", "''");
public static bool IsFullyQualifiedColumnName(this string value)
=> value.TrimEnd().EndsWith("]") && value.IndexOf('[') > 0;

public static (string table, string column) GetFullyQualifiedColumnNameParts(this string value, bool obfuscating = false)
{
Debug.Assert(IsFullyQualifiedColumnName(value));

var openIndex = value.IndexOf('[');
var closeIndex = value.LastIndexOf(']');
var table = value.Substring(0, openIndex);
var column = value.Substring(openIndex + 1, closeIndex - openIndex - 1);

if (obfuscating)
{
table = table.Trim(); // remove any leading or trailing whitespace first

if (IsSquareBraketsRequired(table, column))
{
// Since the plaintext value contains at least one character that results in a fully qualified
// column name that requires square brackets, then, in order to preserve the same semantics
// we must add at least a single char of the same type to the obfuscated value as well.
table = $"{DaxTextObfuscator.ReservedChar_Minus}{table}";
}
}

return (table, column);

static bool IsSquareBraketsRequired(string table, string column)
{
if (table.Length > 0)
{
if ("012345679".Contains(table[0]))
return true; // Table name start with a digit

if (table.Any((c) => c != '_' && !DaxTextObfuscator.CharSet.Contains(c)))
return true; // Table name contains any non-alphabetic characters except for the underscore
}

return column.Contains(']');
}
}

public static string EscapeDax(this string value, int tokenType)
{
// See <TOKEN_TYPE>_action() methods in Dax.Tokenizer.DaxLexer class

switch (tokenType)
{
case DaxToken.TABLE:
case DaxToken.UNTERMINATED_TABLEREF:
return value.Replace("'", "''");

case DaxToken.STRING_LITERAL:
return value.Replace("\"", "\"\"");

case DaxToken.COLUMN_OR_MEASURE:
case DaxToken.UNTERMINATED_COLREF:
return value.Replace("]", "]]");
}

return value;
}
}
2 changes: 1 addition & 1 deletion src/Dax.Vpax.Obfuscator/version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "0.3-beta",
"version": "0.4-beta",
"nugetPackageVersion": {
"semVer": 2.0
},
Expand Down
Loading
Loading