Skip to content
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
3 changes: 3 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
PN_PAM_PUB_KEY: ${{ secrets.SDK_PAM_PUB_KEY }}
PN_PAM_SUB_KEY: ${{ secrets.SDK_PAM_SUB_KEY }}
PN_PAM_SEC_KEY: ${{ secrets.SDK_PAM_SEC_KEY }}
PN_CAT_PUB_KEY: ${{ secrets.SDK_CAT_PUB_KEY }}
PN_CAT_SUB_KEY: ${{ secrets.SDK_CAT_SUB_KEY }}
PN_CAT_SEC_KEY: ${{ secrets.SDK_CAT_SEC_KEY }}
run: dotnet test .\\src\\UnitTests\\PubnubApiPCL.Tests\\PubnubApiPCL.Tests.csproj --verbosity normal --logger "console;verbosity=detailed"
- name: Cancel workflow runs for commit on error
if: failure()
Expand Down
21 changes: 14 additions & 7 deletions .pubnub.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
name: c-sharp
version: "9.0.0"
version: "9.1.0"
schema: 1
scm: github.com/pubnub/c-sharp
changelog:
- date: 2026-10-13
version: v9.1.0
changes:
- type: feature
text: "Categories permission support for Grant and GrantToken apis."
- type: bug
text: "Ensure unsubscribeAll clears subscription state when event engine is enabled."
- date: 2026-09-08
version: v9.0.0
changes:
Expand Down Expand Up @@ -1058,14 +1065,14 @@ features:
- QUERY-PARAM
supported-platforms:
-
version: Pubnub 'C#' 9.0.0
version: Pubnub 'C#' 9.1.0
platforms:
- Windows 10 and up
- Windows Server 2008 and up
frameworks:
- .Net Framework 4.5+
-
version: PubnubPCL 'C#' 9.0.0
version: PubnubPCL 'C#' 9.1.0
platforms:
- Xamarin.Android
- Xamarin.iOS
Expand All @@ -1077,7 +1084,7 @@ supported-platforms:
frameworks:
- .Net 4.5+
-
version: PubnubUWP 'C#' 9.0.0
version: PubnubUWP 'C#' 9.1.0
platforms:
- Windows Phone 10
- Universal Windows Apps
Expand All @@ -1101,7 +1108,7 @@ sdks:
distribution-type: source
distribution-repository: GitHub
package-name: Pubnub
location: https://github.com/pubnub/c-sharp/releases/tag/v9.0.0
location: https://github.com/pubnub/c-sharp/releases/tag/v9.1.0
requires:
-
name: ".Net"
Expand Down Expand Up @@ -1342,7 +1349,7 @@ sdks:
distribution-type: source
distribution-repository: GitHub
package-name: PubNubPCL
location: https://github.com/pubnub/c-sharp/releases/tag/v9.0.0
location: https://github.com/pubnub/c-sharp/releases/tag/v9.1.0
requires:
-
name: ".Net"
Expand Down Expand Up @@ -1693,7 +1700,7 @@ sdks:
distribution-type: source
distribution-repository: GitHub
package-name: PubnubUWP
location: https://github.com/pubnub/c-sharp/releases/tag/v9.0.0
location: https://github.com/pubnub/c-sharp/releases/tag/v9.1.0
requires:
-
name: "Universal Windows Platform Development"
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
v9.1.0 - October 13 2026
-----------------------------
- Added: categories permission support for Grant and GrantToken apis.

- Fixed: ensure unsubscribeAll clears subscription state when event engine is enabled.

v9.0.0 - September 08 2026
-----------------------------
- Added: added Data Sync feature support.
Expand Down
60 changes: 60 additions & 0 deletions src/Api/PubnubApi/EndPoint/Access/GrantOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class GrantOperation : PubnubCoreBase
private string[] pubnubChannelNames;
private string[] pubnubChannelGroupNames;
private string[] pubnubTargetUuids;
private PNGrantCategory[] pubnubCategories;
private string[] pamAuthenticationKeys;
private bool grantWrite;
private bool grantRead;
Expand Down Expand Up @@ -53,6 +54,16 @@ public GrantOperation Uuids(string[] targetUuids)
return this;
}

/// <summary>
/// App Context categories whose metadata the auth keys may list in full.
/// Requires AuthKeys and accepts only the Get permission: Get(true) grants, Get(false) revokes.
/// </summary>
public GrantOperation Categories(PNGrantCategory[] categories)
{
pubnubCategories = categories;
return this;
}

public GrantOperation AuthKeys(string[] authKeys)
{
pamAuthenticationKeys = authKeys;
Expand Down Expand Up @@ -129,6 +140,7 @@ public void Execute(PNCallback<PNAccessManagerGrantResult> callback)
{
throw new InvalidOperationException("Only Get/Update/Delete permissions are allowed for UUID");
}
ValidateCategories();
savedCallback = callback;
GrantAccess(callback);
}
Expand All @@ -143,6 +155,7 @@ public async Task<PNResult<PNAccessManagerGrantResult>> ExecuteAsync()
{
throw new InvalidOperationException("Only Get/Update/Delete permissions are allowed for UUID");
}
ValidateCategories();
return await GrantAccess().ConfigureAwait(false);
}

Expand All @@ -151,6 +164,39 @@ internal void Retry()
GrantAccess(savedCallback);
}

private void ValidateCategories()
{
if (pubnubCategories == null || pubnubCategories.Length == 0)
{
return;
}
if (pubnubCategories.Any(category => !Enum.IsDefined(typeof(PNGrantCategory), category)))
{
throw new InvalidOperationException("Only Channels and Uuids categories are supported");
}
if (pamAuthenticationKeys == null || !pamAuthenticationKeys.Any(auth => !string.IsNullOrEmpty(auth) && auth.Trim().Length > 0))
{
throw new InvalidOperationException("Category grants require at least one auth key");
}
if (grantRead || grantWrite || grantManage || grantDelete || grantUpdate || grantJoin)
{
throw new InvalidOperationException("Only Get permission is allowed for categories");
}
}

private static string GetCategoryQueryValue(PNGrantCategory category)
{
switch (category)
{
case PNGrantCategory.Channels:
return "channels";
case PNGrantCategory.Uuids:
return "uuids";
default:
throw new InvalidOperationException("Only Channels and Uuids categories are supported");
}
}

internal void GrantAccess(PNCallback<PNAccessManagerGrantResult> callback)
{
if (string.IsNullOrEmpty(config.SecretKey) || string.IsNullOrEmpty(config.SecretKey.Trim()) || config.SecretKey.Length <= 0)
Expand All @@ -161,6 +207,7 @@ internal void GrantAccess(PNCallback<PNAccessManagerGrantResult> callback)
"channels", pubnubChannelNames,
"channelGroups", pubnubChannelGroupNames,
"uuids", pubnubTargetUuids,
"categories", pubnubCategories,
"authKeys", pamAuthenticationKeys,
"read", grantRead,
"write", grantWrite,
Expand Down Expand Up @@ -212,6 +259,7 @@ internal async Task<PNResult<PNAccessManagerGrantResult>> GrantAccess()
"channels", pubnubChannelNames,
"channelGroups", pubnubChannelGroupNames,
"uuids", pubnubTargetUuids,
"categories", pubnubCategories,
"authKeys", pamAuthenticationKeys,
"read", grantRead,
"write", grantWrite,
Expand Down Expand Up @@ -277,6 +325,7 @@ private RequestParameter CreateRequestParameter()
List<string> channelList = new List<string>();
List<string> channelGroupList = new List<string>();
List<string> uuidList = new List<string>();
List<string> categoryList = new List<string>();
List<string> authList = new List<string>();

if (pubnubChannelNames != null && pubnubChannelNames.Length > 0)
Expand All @@ -297,6 +346,11 @@ private RequestParameter CreateRequestParameter()
uuidList = uuidList.Where(uuid => !string.IsNullOrEmpty(uuid) && uuid.Trim().Length > 0).Distinct().ToList();
}

if (pubnubCategories != null && pubnubCategories.Length > 0)
{
categoryList = pubnubCategories.Select(GetCategoryQueryValue).Distinct().ToList();
}

if (pamAuthenticationKeys != null && pamAuthenticationKeys.Length > 0)
{
authList = new List<string>(pamAuthenticationKeys);
Expand All @@ -306,6 +360,7 @@ private RequestParameter CreateRequestParameter()
string channelsCommaDelimited = string.Join(",", channelList.OrderBy(x => x).ToArray());
string channelGroupsCommaDelimited = string.Join(",", channelGroupList.OrderBy(x => x).ToArray());
string targetUuidsCommaDelimited = string.Join(",", uuidList.OrderBy(x => x).ToArray());
string categoriesCommaDelimited = string.Join(",", categoryList.OrderBy(x => x).ToArray());
string authKeysCommaDelimited = string.Join(",", authList.OrderBy(x => x).ToArray());

List<string> pathSegments = new List<string>() {
Expand Down Expand Up @@ -338,6 +393,11 @@ private RequestParameter CreateRequestParameter()
requestQueryStringParams.Add("target-uuid", UriUtil.EncodeUriComponent(targetUuidsCommaDelimited, PNOperationType.PNAccessManagerGrant, false, false, false));
}

if (!string.IsNullOrEmpty(categoriesCommaDelimited))
{
requestQueryStringParams.Add("category", UriUtil.EncodeUriComponent(categoriesCommaDelimited, PNOperationType.PNAccessManagerGrant, false, false, false));
}

if (grantTTL > -1)
{
requestQueryStringParams.Add("ttl", grantTTL.ToString(CultureInfo.InvariantCulture));
Expand Down
39 changes: 38 additions & 1 deletion src/Api/PubnubApi/EndPoint/Access/GrantTokenOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class GrantTokenOperation : PubnubCoreBase
private string pubnubAuthorizedUuid = string.Empty;
private string pubnubAuthorizedUserId = string.Empty;
private PNDataSyncProjections pubnubDataSyncProjections;
private PNTokenCategories pubnubCategories;

public GrantTokenOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, IPubnubUnitTest pubnubUnit, EndPoint.TokenManager tokenManager, Pubnub instance) : base(pubnubConfig, jsonPluggableLibrary, pubnubUnit, tokenManager, instance)
{
Expand Down Expand Up @@ -135,6 +136,16 @@ public GrantTokenOperation Patterns(PNTokenPatterns patterns)
return this;
}

/// <summary>
/// Category-level App Context permissions, e.g. listing all channel or uuid metadata.
/// Resource-level or pattern Get permissions do not imply them.
/// </summary>
public GrantTokenOperation Categories(PNTokenCategories categories)
{
this.pubnubCategories = categories;
return this;
}

/// <summary>
/// Assigns Data Sync projections to resources/patterns. The projection name
/// per resource is encoded into the token's meta "pn-projections" section.
Expand Down Expand Up @@ -167,6 +178,7 @@ public GrantTokenOperation QueryParam(Dictionary<string, object> customQueryPara
public void Execute(PNCallback<PNAccessManagerTokenResult> callback)
{
logger?.Trace($"{GetType().Name} Execute invoked");
savedCallbackGrantToken = callback;
GrantAccess(callback);
}

Expand Down Expand Up @@ -194,6 +206,7 @@ internal void GrantAccess(PNCallback<PNAccessManagerTokenResult> callback)
logger?.Debug(() => $"{GetType().Name} " + PubnubLogFormatter.Parameters("GrantToken",
"resources", pubnubResources,
"patterns", pubnubPatterns,
"categories", BuildCategoryPermissions(),
"ttl", grantTTL,
"authorizedUuid", pubnubAuthorizedUuid,
"authorizedUserId", pubnubAuthorizedUserId,
Expand Down Expand Up @@ -275,6 +288,21 @@ private bool AddDataSyncScope(Dictionary<string, object> collection, string key,
return atleastOnePermission;
}

private Dictionary<string, int> BuildCategoryPermissions()
{
Dictionary<string, int> categories = new Dictionary<string, int>();
if (this.pubnubCategories == null) {
return categories;
}
if (this.pubnubCategories.Channels != null && this.pubnubCategories.Channels.Get) {
categories.Add("channels", (int)GrantBitFlag.GET);
}
if (this.pubnubCategories.Uuids != null && this.pubnubCategories.Uuids.Get) {
categories.Add("uuids", (int)GrantBitFlag.GET);
}
return categories;
}

private Dictionary<string, object> BuildProjectionsMeta()
{
if (this.pubnubDataSyncProjections == null) {
Expand Down Expand Up @@ -332,6 +360,7 @@ internal async Task<PNResult<PNAccessManagerTokenResult>> GrantAccess()
logger?.Debug(() => $"{GetType().Name} " + PubnubLogFormatter.Parameters("GrantToken",
"resources", pubnubResources,
"patterns", pubnubPatterns,
"categories", BuildCategoryPermissions(),
"ttl", grantTTL,
"authorizedUuid", pubnubAuthorizedUuid,
"authorizedUserId", pubnubAuthorizedUserId,
Expand Down Expand Up @@ -483,8 +512,13 @@ private RequestParameter CreateRequestParameter()
atleastOnePermission = AddDataSyncScopes(resourcesCollection, this.pubnubResources.DataSync, atleastOnePermission);
atleastOnePermission = AddDataSyncScopes(patternsCollection, this.pubnubPatterns.DataSync, atleastOnePermission);

Dictionary<string, int> categoryBitmaskPermCollection = BuildCategoryPermissions();
if (categoryBitmaskPermCollection.Count > 0) {
atleastOnePermission = true;
}

if (!atleastOnePermission) {
config?.Logger?.Warn("GrantToken At least one permission is needed for at least one or more of uuids/users, channels/spaces, groups or datasync entities/relationships/memberships");
config?.Logger?.Warn("GrantToken At least one permission is needed for at least one or more of uuids/users, channels/spaces, groups, categories or datasync entities/relationships/memberships");
}

Dictionary<string, object> optimizedMeta = new Dictionary<string, object>();
Expand All @@ -503,6 +537,9 @@ private RequestParameter CreateRequestParameter()
{ "patterns", patternsCollection },
{ "meta", optimizedMeta }
};
if (categoryBitmaskPermCollection.Count > 0) {
permissionCollection.Add("categories", categoryBitmaskPermCollection);
}
if (!string.IsNullOrEmpty(this.pubnubAuthorizedUuid) && this.pubnubAuthorizedUuid.Trim().Length > 0) {
permissionCollection.Add("uuid", this.pubnubAuthorizedUuid);
} else if (!string.IsNullOrEmpty(this.pubnubAuthorizedUserId) && this.pubnubAuthorizedUserId.Trim().Length > 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Generic;

namespace PubnubApi.EndPoint
namespace PubnubApi
{
public class CreateChannelParameters
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace PubnubApi.EndPoint
namespace PubnubApi
{
public class DeleteChannelParameters
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace PubnubApi.EndPoint
namespace PubnubApi
{
public class GetChannelParameters
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Generic;

namespace PubnubApi.EndPoint
namespace PubnubApi
{
public class GetChannelsParameters
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Generic;

namespace PubnubApi.EndPoint;
namespace PubnubApi;

public class PNDataSyncChannelResult
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace PubnubApi.EndPoint;
namespace PubnubApi;

public class PNDataSyncDeleteChannelResult
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Generic;

namespace PubnubApi.EndPoint
namespace PubnubApi
{
public class SetChannelParameters
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Generic;

namespace PubnubApi.EndPoint
namespace PubnubApi
{
public class UpdateChannelParameters
{
Expand Down
3 changes: 2 additions & 1 deletion src/Api/PubnubApi/EndPoint/DataSync/DataSync.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using PubnubApi.EndPoint;

namespace PubnubApi.EndPoint;
namespace PubnubApi;

/// <summary>
/// Entrypoint for PubNub Data Sync operations
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;

namespace PubnubApi.EndPoint
namespace PubnubApi
{
/// <summary>
/// Parameters for creating a new entity via DataSync.
Expand Down
Loading
Loading