Skip to content

Commit cd4446b

Browse files
committed
feat: add Namespaces permission scope for manage_namespaces (Weaviate 1.38+)
Map the 1.38 RBAC action manage_namespaces into the client permission model instead of leaving it deserialized-but-unmapped: - Permissions.Namespaces scope with a Manage flag and a NamespacesResource(Namespace = "*") filter (exact name or regex pattern), mirroring the Backups manage-only scope shape and the python client's Permissions.namespaces factory / _NamespacesPermission ({"namespaces": {"namespace": ...}} wire form). - Two-way mapping: ToDto emits the namespaces resource filter with the action; Permissions.Parse groups by the namespaces resource like every other scope. - Drop the temporary Manage_namespaces exclusion in the AllPermissionActions_AreMentioned guard; the action is now genuinely covered by parse/serialize round-trip unit tests. - CreateRoleWithNamespacesPermission integration test, gated RequireVersion("1.38.0"), verified against a 1.38.4 RBAC node. - PublicAPI.Unshipped.txt entries for the new public surface.
1 parent 5bf263a commit cd4446b

5 files changed

Lines changed: 216 additions & 4 deletions

File tree

src/Weaviate.Client.Tests/Integration/TestRbacRoles.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,46 @@ [new Permissions.Roles(roleName, null) { Read = true }],
8080
}
8181
}
8282

83+
/// <summary>
84+
/// Tests that create role with namespaces permission round trips (Weaviate 1.38+)
85+
/// </summary>
86+
[Fact]
87+
public async Task CreateRoleWithNamespacesPermission()
88+
{
89+
RequireVersion("1.38.0");
90+
var roleName = MakeRoleName("namespaces");
91+
try
92+
{
93+
await _weaviate.Roles.Delete(roleName, TestContext.Current.CancellationToken);
94+
var created = await _weaviate.Roles.Create(
95+
roleName,
96+
[new Permissions.Namespaces("*") { Manage = true }],
97+
TestContext.Current.CancellationToken
98+
);
99+
Assert.NotNull(created);
100+
Assert.Equal(roleName, created.Name);
101+
Assert.Single(created.Permissions);
102+
var scope = Assert.IsType<Permissions.Namespaces>(created.Permissions.Single());
103+
Assert.True(scope.Manage);
104+
Assert.Equal("*", scope.Resource.Namespace);
105+
106+
var fetched = await _weaviate.Roles.Get(
107+
roleName,
108+
TestContext.Current.CancellationToken
109+
);
110+
Assert.NotNull(fetched);
111+
Assert.Equal(roleName, fetched!.Name);
112+
Assert.Single(fetched.Permissions);
113+
var fetchedScope = Assert.IsType<Permissions.Namespaces>(fetched.Permissions.Single());
114+
Assert.True(fetchedScope.Manage);
115+
Assert.Equal("*", fetchedScope.Resource.Namespace);
116+
}
117+
finally
118+
{
119+
await _weaviate.Roles.Delete(roleName, TestContext.Current.CancellationToken);
120+
}
121+
}
122+
83123
/// <summary>
84124
/// Tests that create role conflict
85125
/// </summary>

src/Weaviate.Client.Tests/Unit/PermissionsScopeTests.cs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,54 @@ public void Backups_Aggregates_ManageBackupsOnly()
389389
Assert.True(backups[0].Manage);
390390
}
391391

392+
/// <summary>
393+
/// Tests that namespaces aggregates manage namespaces only (Weaviate 1.38+)
394+
/// </summary>
395+
[Fact]
396+
public void Namespaces_Aggregates_ManageNamespacesOnly()
397+
{
398+
var resource = new Rest.Dto.Namespaces { Namespace = "ns-.*" };
399+
var permissions = new List<Rest.Dto.Permission>
400+
{
401+
new()
402+
{
403+
Action = Weaviate.Client.Rest.Dto.PermissionAction.Manage_namespaces,
404+
Namespaces = resource,
405+
},
406+
};
407+
var namespaces = Permissions
408+
.Namespaces.Parse(permissions)
409+
.Cast<Permissions.Namespaces>()
410+
.ToList();
411+
Assert.Single(namespaces);
412+
Assert.True(namespaces[0].Manage);
413+
Assert.Equal("ns-.*", namespaces[0].Resource.Namespace);
414+
}
415+
416+
/// <summary>
417+
/// Tests that namespaces round trips through the dto (Weaviate 1.38+)
418+
/// </summary>
419+
[Fact]
420+
public void Namespaces_ToDto_RoundTrips()
421+
{
422+
var scope = new Permissions.Namespaces("team-a") { Manage = true };
423+
424+
var dtos = scope.ToDto().ToList();
425+
426+
var dto = Assert.Single(dtos);
427+
Assert.Equal(Weaviate.Client.Rest.Dto.PermissionAction.Manage_namespaces, dto.Action);
428+
Assert.NotNull(dto.Namespaces);
429+
Assert.Equal("team-a", dto.Namespaces!.Namespace);
430+
431+
var parsed = Assert.Single(Permissions.Namespaces.Parse(dtos));
432+
var roundTripped = Assert.IsType<Permissions.Namespaces>(parsed);
433+
Assert.True(roundTripped.Manage);
434+
Assert.Equal("team-a", roundTripped.Resource.Namespace);
435+
436+
// No actions set -> no permission entries are emitted.
437+
Assert.Empty(new Permissions.Namespaces("team-a").ToDto());
438+
}
439+
392440
/// <summary>
393441
/// Tests that all permission actions are mentioned
394442
/// </summary>
@@ -407,14 +455,11 @@ public void AllPermissionActions_AreMentioned()
407455
|| a.ToString() == "Update_backup"
408456
|| a.ToString() == "Delete_backup"
409457
);
410-
// manage_namespaces (Weaviate 1.38+) is deserialized but has no client-side
411-
// permission scope yet; Permissions.Parse leaves it unmapped. Remove this
412-
// exclusion once a Namespaces permission scope is implemented.
413-
allActions.RemoveAll(a => a.ToString() == "Manage_namespaces");
414458
// List of actions covered by tests
415459
var testedActions = new HashSet<string>
416460
{
417461
"Manage_backups",
462+
"Manage_namespaces",
418463
"Read_cluster",
419464
"Create_data",
420465
"Read_data",

src/Weaviate.Client/Models/PermissionResource.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ public record ReplicateResource(string? Collection = "*", string? Shard = "*");
7070
/// <param name="Alias">Optional alias name (defaults to "*" for all aliases)</param>
7171
public record AliasesResource(string? Collection = "*", string? Alias = "*");
7272

73+
/// <summary>
74+
/// Represents a namespaces resource, optionally scoped to a namespace. Requires Weaviate 1.38 or later.
75+
/// </summary>
76+
/// <param name="Namespace">Optional namespace name or regex pattern (defaults to "*" for all namespaces)</param>
77+
public record NamespacesResource(string? Namespace = "*");
78+
7379
/// <summary>
7480
/// The permission resource extensions class
7581
/// </summary>
@@ -191,6 +197,16 @@ internal static Rest.Dto.Aliases ToDto(this AliasesResource resource)
191197
return new Rest.Dto.Aliases { Collection = resource.Collection, Alias = resource.Alias };
192198
}
193199

200+
/// <summary>
201+
/// Returns the dto using the specified resource
202+
/// </summary>
203+
/// <param name="resource">The resource</param>
204+
/// <returns>The rest dto namespaces</returns>
205+
internal static Rest.Dto.Namespaces ToDto(this NamespacesResource resource)
206+
{
207+
return new Rest.Dto.Namespaces { Namespace = resource.Namespace };
208+
}
209+
194210
/// <summary>
195211
/// Returns the model using the specified resource
196212
/// </summary>
@@ -421,4 +437,26 @@ internal static PermissionScope ToModel(
421437
Delete = actions.Contains(Rest.Dto.PermissionAction.Delete_collections),
422438
};
423439
}
440+
441+
/// <summary>
442+
/// Returns the model using the specified resource
443+
/// </summary>
444+
/// <param name="resource">The resource</param>
445+
/// <param name="permissions">The permissions</param>
446+
/// <returns>The permission scope</returns>
447+
internal static PermissionScope ToModel(
448+
this Rest.Dto.Namespaces resource,
449+
IEnumerable<Rest.Dto.Permission> permissions
450+
)
451+
{
452+
var actions = permissions
453+
.Where(p => p.Namespaces == resource)
454+
.Select(p => p.Action)
455+
.ToHashSet();
456+
457+
return new Permissions.Namespaces(resource.Namespace)
458+
{
459+
Manage = actions.Contains(Rest.Dto.PermissionAction.Manage_namespaces),
460+
};
461+
}
424462
}

src/Weaviate.Client/Models/RbacPermissions.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,74 @@ internal static List<PermissionScope> Parse(IEnumerable<Rest.Dto.Permission> inf
267267
}
268268
}
269269

270+
/// <summary>
271+
/// The namespaces class. Requires Weaviate 1.38 or later.
272+
/// </summary>
273+
/// <seealso cref="PermissionScope"/>
274+
public class Namespaces : PermissionScope
275+
{
276+
/// <summary>
277+
/// Gets the value of the resource
278+
/// </summary>
279+
public NamespacesResource Resource { get; }
280+
281+
/// <summary>
282+
/// Gets or sets the value of the manage
283+
/// </summary>
284+
public bool Manage { get; set; }
285+
286+
/// <summary>
287+
/// Initializes a new instance of the <see cref="Namespaces"/> class
288+
/// </summary>
289+
/// <param name="namespace">The namespace name or regex pattern</param>
290+
public Namespaces(string? @namespace)
291+
: this(new NamespacesResource(@namespace)) { }
292+
293+
/// <summary>
294+
/// Initializes a new instance of the <see cref="Namespaces"/> class
295+
/// </summary>
296+
/// <param name="resource">The resource</param>
297+
/// <exception cref="ArgumentNullException"></exception>
298+
Namespaces(NamespacesResource resource)
299+
{
300+
Resource = resource ?? throw new ArgumentNullException(nameof(resource));
301+
}
302+
303+
/// <summary>
304+
/// Returns the dto
305+
/// </summary>
306+
/// <returns>An enumerable of rest dto permission</returns>
307+
internal override IEnumerable<Rest.Dto.Permission> ToDto()
308+
{
309+
var permissions = new[]
310+
{
311+
(Action: Rest.Dto.PermissionAction.Manage_namespaces, Allowed: Manage),
312+
};
313+
314+
return permissions
315+
.Where(p => p.Allowed)
316+
.Select(p => new Rest.Dto.Permission
317+
{
318+
Namespaces = Resource.ToDto(),
319+
Action = p.Action,
320+
});
321+
}
322+
323+
/// <summary>
324+
/// Parses the infos
325+
/// </summary>
326+
/// <param name="infos">The infos</param>
327+
/// <returns>A list of permission scope</returns>
328+
internal static List<PermissionScope> Parse(IEnumerable<Rest.Dto.Permission> infos)
329+
{
330+
return infos
331+
.Where(i => i.Namespaces != null)
332+
.GroupBy(i => i.Namespaces!)
333+
.Select(group => group.Key.ToModel(group.AsEnumerable()))
334+
.ToList();
335+
}
336+
}
337+
270338
/// <summary>
271339
/// The mcp class
272340
/// </summary>
@@ -988,6 +1056,7 @@ internal static List<PermissionScope> Parse(IEnumerable<Rest.Dto.Permission> inf
9881056
scopes.AddRange(Alias.Parse(infos));
9891057
scopes.AddRange(Data.Parse(infos));
9901058
scopes.AddRange(Backups.Parse(infos));
1059+
scopes.AddRange(Namespaces.Parse(infos));
9911060
scopes.AddRange(Mcp.Parse(infos));
9921061
scopes.AddRange(Cluster.Parse(infos));
9931062
scopes.AddRange(Nodes.Parse(infos));

src/Weaviate.Client/PublicAPI.Unshipped.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,23 @@ Weaviate.Client.Models.Vectorizer.Text2VecGoogle.Location.set -> void
1010
Weaviate.Client.VectorizerFactory.Text2VecAWSBedrock(string! region, string! model, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig!
1111
Weaviate.Client.VectorizerFactory.Text2VecAWSSagemaker(string! region, string! endpoint, string? targetModel = null, string? targetVariant = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig!
1212
Weaviate.Client.VectorizerFactory.Text2VecGoogleVertex(string? apiEndpoint = null, string? model = null, string? projectId = null, string? titleProperty = null, int? dimensions = null, string? taskType = null, bool? vectorizeCollectionName = null, string? location = null) -> Weaviate.Client.Models.VectorizerConfig!
13+
override Weaviate.Client.Models.NamespacesResource.Equals(object? obj) -> bool
14+
override Weaviate.Client.Models.NamespacesResource.GetHashCode() -> int
15+
override Weaviate.Client.Models.NamespacesResource.ToString() -> string!
16+
static Weaviate.Client.Models.NamespacesResource.operator !=(Weaviate.Client.Models.NamespacesResource? left, Weaviate.Client.Models.NamespacesResource? right) -> bool
17+
static Weaviate.Client.Models.NamespacesResource.operator ==(Weaviate.Client.Models.NamespacesResource? left, Weaviate.Client.Models.NamespacesResource? right) -> bool
18+
virtual Weaviate.Client.Models.NamespacesResource.<Clone>$() -> Weaviate.Client.Models.NamespacesResource!
19+
virtual Weaviate.Client.Models.NamespacesResource.EqualityContract.get -> System.Type!
20+
virtual Weaviate.Client.Models.NamespacesResource.Equals(Weaviate.Client.Models.NamespacesResource? other) -> bool
21+
virtual Weaviate.Client.Models.NamespacesResource.PrintMembers(System.Text.StringBuilder! builder) -> bool
22+
Weaviate.Client.Models.NamespacesResource
23+
Weaviate.Client.Models.NamespacesResource.Deconstruct(out string? Namespace) -> void
24+
Weaviate.Client.Models.NamespacesResource.Namespace.get -> string?
25+
Weaviate.Client.Models.NamespacesResource.Namespace.init -> void
26+
Weaviate.Client.Models.NamespacesResource.NamespacesResource(string? Namespace = "*") -> void
27+
Weaviate.Client.Models.NamespacesResource.NamespacesResource(Weaviate.Client.Models.NamespacesResource! original) -> void
28+
Weaviate.Client.Models.Permissions.Namespaces
29+
Weaviate.Client.Models.Permissions.Namespaces.Manage.get -> bool
30+
Weaviate.Client.Models.Permissions.Namespaces.Manage.set -> void
31+
Weaviate.Client.Models.Permissions.Namespaces.Namespaces(string? namespace) -> void
32+
Weaviate.Client.Models.Permissions.Namespaces.Resource.get -> Weaviate.Client.Models.NamespacesResource!

0 commit comments

Comments
 (0)