Skip to content

Commit 247d812

Browse files
committed
Simplify security conditions
1 parent 06a7083 commit 247d812

10 files changed

Lines changed: 84 additions & 140 deletions

File tree

src/AasSecurity/ISecurityService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,6 @@ bool AuthorizeRequest(string accessRole,
5252
string? policy = null,
5353
List<Claim> tokenClaims = null);
5454

55-
string GetSecurityRules(out List<Dictionary<string, string>> condition);
55+
string GetSecurityRules();
5656
}
57-
}
57+
}

src/AasSecurity/SecurityService.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ namespace AasSecurity
5151
{
5252
public class SecurityService : ISecurityService, IContractSecurityRules
5353
{
54-
public static List<Dictionary<string, string>>? _condition = new List<Dictionary<string, string>>();
55-
5654
public static AllAccessPermissionRules? _accessRules = null;
5755

5856
public SecurityService()
@@ -85,7 +83,6 @@ public void parseAccessRuleFile()
8583
ClearSecurityRules();
8684
grammar.ParseAccessRules(expression);
8785
_accessRules = QueryGrammarJSON._accessRules;
88-
_condition = QueryGrammarJSON.allAccessRuleExpressions;
8986
}
9087

9188
public List<AccessPermissionRule>? GetAccessRules(string accessRole, string neededRightsClaim, string? httpRoute = null,
@@ -358,7 +355,6 @@ private string HandleJsonFormatedTokenClaim(string value)
358355

359356
public void ClearSecurityRules()
360357
{
361-
_condition.Clear();
362358
_accessRules = null;
363359
GlobalSecurityVariables.SecurityRoles.Clear();
364360
}
@@ -1361,14 +1357,8 @@ private static bool MatchApiOperation(string apiOperation, string operation)
13611357
return true;
13621358
}
13631359

1364-
public string GetSecurityRules(out List<Dictionary<string, string>> condition)
1360+
public string GetSecurityRules()
13651361
{
1366-
condition = new List<Dictionary<string, string>>();
1367-
if (_condition != null)
1368-
{
1369-
condition = _condition;
1370-
}
1371-
13721362
string rules = "";
13731363

13741364
foreach (var r in GlobalSecurityVariables.SecurityRoles)

src/AasxServerBlazor/Pages/TreePage.razor

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,7 @@
135135
text = text.Replace("%BLAZOR%", Program.externalBlazor);
136136
if (text.Contains("%ACCESSRULES%"))
137137
{
138-
List<Dictionary<string, string>> condition = [];
139-
var accessRules = SecService.GetSecurityRules(out condition);
138+
var accessRules = SecService.GetSecurityRules();
140139
<span><b>AAS API</b></span>
141140
<br />
142141
<table class="table table-bordered table-sm">
@@ -177,22 +176,6 @@
177176
}
178177
</tbody>
179178
</table>
180-
<br />
181-
<span><b>AAS Data</b></span>
182-
<br />
183-
@foreach (var dict in condition)
184-
{
185-
<table class="table table-bordered table-sm">
186-
@foreach (var kvp in dict)
187-
{
188-
<tr>
189-
<td>@kvp.Key</td>
190-
<td>@kvp.Value</td>
191-
</tr>
192-
}
193-
</table>
194-
<br />
195-
}
196179
}
197180
else
198181
{

src/AasxServerDB.Tests/QueryTests.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,71 @@ public void ReadPagedSubmodels_NoAuthSecurityFilter_MatchesExpectedSubset()
509509
result.Select(sm => sm.IdShort).Distinct().Should().OnlyContain(idShort => idShort == "Nameplate" || idShort == "TechnicalData");
510510
}
511511

512+
/// <summary>
513+
/// Regression for the single-submodel GET path (<see cref="CrudOperator.ReadSubmodel"/>): the
514+
/// element-level access-rule FILTER (sme idShort) must be applied there too, not only by the paged
515+
/// list path. Guards the bug where <c>ApplySmeSqlFilterConditions</c> read the always-empty
516+
/// <c>FilterConditions</c> bucket instead of <c>FormulaConditions</c>, so GET /submodels/{id}
517+
/// returned all elements unfiltered while the tree view filtered correctly.
518+
/// </summary>
519+
[Fact]
520+
public void ReadSubmodelById_NoAuthSecurityFilter_FiltersSubmodelElements()
521+
{
522+
const string expression = """
523+
{
524+
"AllAccessPermissionRules": {
525+
"rules": [
526+
{
527+
"ACL": {
528+
"ATTRIBUTES": [ { "CLAIM": "isNotAuthenticated" } ],
529+
"RIGHTS": [ "READ" ],
530+
"ACCESS": "ALLOW"
531+
},
532+
"OBJECTS": [ { "ROUTE": "/submodels" } ],
533+
"FORMULA": {
534+
"$or": [
535+
{ "$eq": [ { "$field": "$sm#idShort" }, { "$strVal": "Nameplate" } ] },
536+
{ "$eq": [ { "$field": "$sm#idShort" }, { "$strVal": "TechnicalData" } ] }
537+
]
538+
},
539+
"FILTER": {
540+
"FRAGMENT": "xxx",
541+
"CONDITION": {
542+
"$or": [
543+
{ "$starts-with": [ { "$field": "$sme#idShort" }, { "$strVal": "General" } ] },
544+
{ "$starts-with": [ { "$field": "$sme#idShort" }, { "$strVal": "Manufacturer" } ] }
545+
]
546+
}
547+
}
548+
}
549+
]
550+
}
551+
}
552+
""";
553+
554+
const string nameplateId = "https://i4d.de/T/2900542/submodel/Nameplate";
555+
556+
using var db = _fixture.CreateDbContext();
557+
var securitySqlConditions = LoadSubmodelAccessRuleSqlConditions(expression);
558+
559+
// Baseline: full submodel without security — must contain elements the FILTER is meant to drop.
560+
var full = CrudOperator.ReadSubmodel(db, submodelIdentifier: nameplateId, securitySqlConditions: null, skipAllowCheck: true);
561+
full.Should().NotBeNull("the Nameplate submodel exists in the test database");
562+
var fullIdShorts = full!.SubmodelElements!.Select(e => e.IdShort).ToList();
563+
fullIdShorts.Should().Contain(s => !(s!.StartsWith("General") || s.StartsWith("Manufacturer")),
564+
"the baseline must include elements the FILTER is expected to remove");
565+
566+
// With security: only top-level elements allowed by the FILTER (idShort General*/Manufacturer*) remain.
567+
var filtered = CrudOperator.ReadSubmodel(db, submodelIdentifier: nameplateId, securitySqlConditions: securitySqlConditions);
568+
filtered.Should().NotBeNull("Nameplate is allowed by the FORMULA");
569+
var filteredIdShorts = filtered!.SubmodelElements!.Select(e => e.IdShort).ToList();
570+
571+
filteredIdShorts.Should().NotBeEmpty();
572+
filteredIdShorts.Should().OnlyContain(s => s!.StartsWith("General") || s.StartsWith("Manufacturer"));
573+
filteredIdShorts.Count.Should().BeLessThan(fullIdShorts.Count,
574+
"the element-level FILTER must remove non-matching elements on the single-submodel GET path");
575+
}
576+
512577
// -------------------------------------------------------------------------
513578
// Access rule with $attribute(CLAIM(token:sub)) — regression cover for the
514579
// CLAIM substitution that was lost when SecurityService.GetCondition was

src/AasxServerDB.Tests/SqlConditionsClaimScopeTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,6 @@ public void ParseAccessRules_CombinesFormulaAndFilterPerRuleBeforeOrMergingRules
192192
overall.IndexOf("RuleOne", StringComparison.Ordinal).Should().BeLessThan(overall.IndexOf("FilterOne", StringComparison.Ordinal));
193193
overall.IndexOf("FilterOne", StringComparison.Ordinal).Should().BeLessThan(overall.IndexOf("RuleTwo", StringComparison.Ordinal));
194194
overall.IndexOf("RuleTwo", StringComparison.Ordinal).Should().BeLessThan(overall.IndexOf("FilterTwo", StringComparison.Ordinal));
195-
merged.FilterConditions.Values.Should().OnlyContain(value => string.IsNullOrWhiteSpace(value));
196195
}
197196

198197
private static LogicalExpression ClaimAttribute(string claimType)

src/AasxServerDB/CrudOperator.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -626,8 +626,8 @@ private static IQueryable<SMESet> ApplySmeSqlFilterConditions(AasContext db, IQu
626626
try
627627
{
628628
var single = CloneSqlConditions(sqlConditions);
629-
var existing = single.FilterConditions.GetValueOrDefault("sm", "");
630-
single.FilterConditions["sm"] = AppendSqlAnd(existing, $"\"Id\" = {smSet.Id}");
629+
var existing = single.FormulaConditions.GetValueOrDefault("sm", "");
630+
single.FormulaConditions["sm"] = AppendSqlAnd(existing, $"\"Id\" = {smSet.Id}");
631631

632632
var raw = Query.BuildRawSqlFromSqlConditions(
633633
single,
@@ -654,7 +654,6 @@ private static SqlConditions CloneSqlConditions(SqlConditions src)
654654
{
655655
var dst = new SqlConditions { Select = src.Select };
656656
foreach (var kv in src.FormulaConditions) dst.FormulaConditions[kv.Key] = kv.Value;
657-
foreach (var kv in src.FilterConditions) dst.FilterConditions[kv.Key] = kv.Value;
658657
foreach (var kv in src.FormulaConditionsCSharp) dst.FormulaConditionsCSharp[kv.Key] = kv.Value;
659658
dst.Paths.AddRange(src.Paths);
660659
dst.Matches.AddRange(src.Matches);
@@ -692,9 +691,7 @@ private static List<SmeMerged> GetSmeMerged(AasContext db, IQueryable<SMESet>? q
692691
return null;
693692

694693
IQueryable<ValueSet> valueSets = db.ValueSets;
695-
var valueSql = AppendSqlAnd(
696-
sqlConditions?.FormulaConditions.GetValueOrDefault("value", ""),
697-
sqlConditions?.FilterConditions.GetValueOrDefault("value", ""));
694+
var valueSql = sqlConditions?.FormulaConditions.GetValueOrDefault("value", "");
698695
if (!string.IsNullOrWhiteSpace(valueSql))
699696
{
700697
valueSets = QueryValueRaw(db, valueSql).AsQueryable();

src/AasxServerDB/Query.cs

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -381,16 +381,6 @@ internal QueryResult GetQueryData(bool noSecurity, AasContext db,
381381
{
382382
var aasDB = aasList[i];
383383
var aas = ReadAssetAdministrationShell(db, aasDB: ref aasDB);
384-
var filterAas = effectiveSqlConditions?.FilterConditions.GetValueOrDefault("aas", "");
385-
if (!string.IsNullOrWhiteSpace(filterAas) && filterAas != "$SKIP")
386-
{
387-
if (filterAas.Contains("globalAssetId"))
388-
{
389-
aas.Submodels = null;
390-
aas.Description = null;
391-
aas.DisplayName = null;
392-
}
393-
}
394384
if (aas != null)
395385
{
396386
shells.Add(aas);
@@ -1570,13 +1560,10 @@ private static List<int> CombineTablesLEFT(
15701560
if (sqlConditions == null)
15711561
throw new InvalidOperationException("CombineTablesLEFT requires SqlConditions.");
15721562

1573-
var sqlAasMerged = AppendAndCondition(
1574-
NormalizeSqlAliases(sqlConditions.FormulaConditions.GetValueOrDefault("aas", "")),
1575-
NormalizeSqlAliases(sqlConditions.FilterConditions.GetValueOrDefault("aas", "")));
1563+
var sqlAasMerged = NormalizeSqlAliases(sqlConditions.FormulaConditions.GetValueOrDefault("aas", ""));
15761564
var sqlOverallCondition = NormalizeSqlAliases(sqlConditions.FormulaConditions.GetValueOrDefault("all", ""));
1577-
var sqlFilterAllCondition = NormalizeSqlAliases(sqlConditions.FilterConditions.GetValueOrDefault("all", ""));
15781565

1579-
var overallFieldCondition = AppendAndCondition(sqlOverallCondition, sqlFilterAllCondition);
1566+
var overallFieldCondition = sqlOverallCondition;
15801567

15811568
// Do not join AASSets when the AAS scope is only a tautology (e.g. (1=1) from folding) — same idea as "all" without "aas".
15821569
var restrictAAS = !SqlConditionIsPureTautology(sqlAasMerged);
@@ -1648,21 +1635,10 @@ ORDER BY 1
16481635
int pageSize,
16491636
IReadOnlyList<string>? queryFlags = null)
16501637
{
1651-
// Inner AAS/SM subquery WHERE: Formula + per-scope Filter (e.g. security), same pattern as sme/value.
1652-
var whereAas = AppendAndCondition(
1653-
NormalizeSqlAliases(sc.FormulaConditions.GetValueOrDefault("aas", "")),
1654-
NormalizeSqlAliases(sc.FilterConditions.GetValueOrDefault("aas", "")));
1655-
var whereSm = AppendAndCondition(
1656-
NormalizeSqlAliases(sc.FormulaConditions.GetValueOrDefault("sm", "")),
1657-
NormalizeSqlAliases(sc.FilterConditions.GetValueOrDefault("sm", "")));
1658-
// Per-scope FILTER (e.g. security); inner SME/Value JOIN Vorfilter must not use Formula["sme"] alone.
1659-
var whereSme = AppendAndCondition(
1660-
NormalizeSqlAliases(sc.FormulaConditions.GetValueOrDefault("sme", "")),
1661-
NormalizeSqlAliases(sc.FilterConditions.GetValueOrDefault("sme", "")));
1662-
var whereVal = AppendAndCondition(
1663-
NormalizeSqlAliases(sc.FormulaConditions.GetValueOrDefault("value", "")),
1664-
NormalizeSqlAliases(sc.FilterConditions.GetValueOrDefault("value", "")));
1665-
var filterAll = NormalizeSqlAliases(sc.FilterConditions.GetValueOrDefault("all", ""));
1638+
var whereAas = NormalizeSqlAliases(sc.FormulaConditions.GetValueOrDefault("aas", ""));
1639+
var whereSm = NormalizeSqlAliases(sc.FormulaConditions.GetValueOrDefault("sm", ""));
1640+
var whereSme = NormalizeSqlAliases(sc.FormulaConditions.GetValueOrDefault("sme", ""));
1641+
var whereVal = NormalizeSqlAliases(sc.FormulaConditions.GetValueOrDefault("value", ""));
16661642

16671643
var withUnion = queryFlags != null && queryFlags.Contains("$UNION");
16681644
var withTemp = queryFlags != null && queryFlags.Contains("$TEMPTABLE");
@@ -1671,7 +1647,7 @@ ORDER BY 1
16711647
var useLegacySmeJoin = queryFlags != null && queryFlags.Contains("$LEGACYSMEJOIN");
16721648

16731649
// Resolve placeholder references
1674-
var overall = AppendAndCondition(NormalizeSqlAliases(sc.FormulaConditions.GetValueOrDefault("all", "")), filterAll);
1650+
var overall = NormalizeSqlAliases(sc.FormulaConditions.GetValueOrDefault("all", ""));
16751651
var pathNum = 1;
16761652
var matchNum = 1;
16771653
foreach (var path in sc.Paths) overall = overall.Replace($"$${path.Placeholder}$$", $"(p{pathNum++}.SMId IS NOT NULL)");

src/Contracts/JsonData.cs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,8 @@ public class Query
106106
[JsonProperty("$condition")]
107107
public LogicalExpression Condition { get; set; }
108108

109-
public Dictionary<string, string> _query_conditions = [];
110-
111109
[JsonProperty("$filter")]
112110
public LogicalExpression Filter { get; set; }
113-
114-
public Dictionary<string, string> _filter_conditions = [];
115111
}
116112

117113
public class LogicalExpression
@@ -121,8 +117,6 @@ public class LogicalExpression
121117

122118
[JsonExtensionData] private IDictionary<string, JToken> _data;
123119

124-
public string _expression = "";
125-
126120
[OnDeserialized]
127121
private void OnDeserialized(StreamingContext context)
128122
{
@@ -250,12 +244,9 @@ public class AccessPermissionRule
250244
[JsonProperty("USEFILTER")]
251245
public string? UseFilter { get; set; }
252246

253-
public Dictionary<string, string> _formula_conditions = [];
254-
public Dictionary<string, string> _filter_conditions = [];
255-
256-
/// <summary>Direct SQL equivalent of <see cref="_formula_conditions"/> — populated by <see cref="QueryGrammarJSON.ParseAccessRules"/> alongside the LINQ strings.</summary>
247+
/// <summary>SQL conditions produced from <see cref="Formula"/>.</summary>
257248
public SqlConditions? _formula_sqlConditions;
258-
/// <summary>Direct SQL equivalent of <see cref="_filter_conditions"/> — populated by <see cref="QueryGrammarJSON.ParseAccessRules"/> alongside the LINQ strings.</summary>
249+
/// <summary>SQL conditions produced from <see cref="Filter"/>.</summary>
259250
public SqlConditions? _filter_sqlConditions;
260251
}
261252
public class Filter

0 commit comments

Comments
 (0)