Restricting File Access by User or Group with CBFS Filter


NTFS permissions are not always the right place to enforce an access policy. They are set per file or folder, they are visible and editable by anyone with sufficient rights over that object, and they do not travel with an application's own notion of roles or tenants. An application that needs to make its own access decisions, independent of whatever ACLs happen to be set on disk, needs a way to identify the user behind a file request and check that identity against its own policy at the moment the request is made. CBFilter provides this through GetOriginatorToken, which retrieves the security token of the process that initiated a filesystem operation.

Retrieving the Token

GetOriginatorToken can be called from any synchronous Before* or After* event handler and returns a handle to the requesting process's security token:

long token = filter.GetOriginatorToken(); if (token == INVALID_HANDLE_VALUE) { // The token could not be retrieved; decide how your policy should // handle this case, typically by denying the request. }

A few restrictions apply to this method, and they shape where you should call it from:

  • It cannot be called from Notify* event handlers unless the CaptureTokenForNotifyEvents configuration setting is enabled first.
  • It must be called on the same thread that fired the event, and it cannot be used from a CleanupContext handler.
  • It is not recommended for use in events that operate on already-open file handles, since those can be triggered by system components such as the cache or memory manager rather than the process you actually intend to check. BeforeCreateFile and BeforeOpenFile are the events CBFilter's own documentation recommends for this purpose; if the identity is needed again later for the same file, store it via the HandleContext parameter rather than re-querying it in a later event.

When you are finished with the handle, close it with filter.CloseHandle, which exists specifically as a safe substitute for the raw Windows CloseHandle function when closing handles obtained from CBFilter methods:

filter.CloseHandle(token);

Getting the SID from the Token

The handle returned by GetOriginatorToken is a standard Windows security token and can be passed directly to the Win32 GetTokenInformation function to retrieve the TOKEN_USER structure, which contains the user's SID:

``` [DllImport("advapi32.dll", SetLastError = true)] static extern bool GetTokenInformation(IntPtr TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, IntPtr TokenInformation, int TokenInformationLength, out int ReturnLength);

[DllImport("advapi32.dll", SetLastError = true)] static extern bool ConvertSidToStringSid(IntPtr pSid, out IntPtr ptrSid);

void BeforeCreateFile(object sender, CBFilterBeforeCreateFileEventArgs e) { long token = filter.GetOriginatorToken(); if (token == INVALID_HANDLE_VALUE) { e.ResultCode = 5; // ERROR_ACCESS_DENIED e.ProcessRequest = false; return; }

try { string sid = GetSidFromToken(new IntPtr(token)); if (!IsAuthorizedSid(sid)) { e.ResultCode = 5; // ERROR_ACCESS_DENIED e.ProcessRequest = false; } } finally { filter.CloseHandle(token); } } ```

GetSidFromToken above is application code: call GetTokenInformation twice, once with a null buffer to learn the required size via ReturnLength, then again with a buffer of that size to retrieve the TOKEN_USER structure, and convert the Sid field with ConvertSidToStringSid (or LookupAccountSid if you want an account name instead of a raw SID string). This is standard Windows token handling and is not specific to CBFilter; the Microsoft documentation for GetTokenInformation covers the buffer-sizing pattern in full.

Checking Group Membership Instead of a Single SID

An allow list of individual SIDs does not scale well past a handful of users. If your policy is based on group membership instead, CheckTokenMembership is generally a better fit than resolving full group lists yourself: pass it the token and the SID of the group you care about, and it reports whether that token is a member, accounting for the same enabled/disabled group logic Windows itself uses for access checks.

``` [DllImport("advapi32.dll", SetLastError = true)] static extern bool CheckTokenMembership(IntPtr TokenHandle, IntPtr SidToCheck, out bool IsMember);

[DllImport("advapi32.dll", SetLastError = true)] static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, int ImpersonationLevel, int TokenType, out IntPtr phNewToken); ```

CheckTokenMembership requires an impersonation token, and the token returned by GetOriginatorToken is a primary token, not an impersonation token, in the typical case of a request originating from a regular local process. Passing a primary token to CheckTokenMembership directly is not supported and can produce unreliable results, so convert it first with DuplicateTokenEx, requesting SecurityImpersonation as the impersonation level and TokenImpersonation as the token type:

``` IntPtr impersonationToken; if (!DuplicateTokenEx(new IntPtr(token), TOKEN_QUERY, IntPtr.Zero, SecurityImpersonation, TokenImpersonation, out impersonationToken)) { // Could not duplicate the token; deny by default. return; }

try { bool isMember; if (CheckTokenMembership(impersonationToken, groupSid, out isMember) && isMember) { // Token belongs to the group. } } finally { CloseHandle(impersonationToken); // Raw Windows CloseHandle; this handle did not come from CBFilter. } ```

The handle produced by DuplicateTokenEx is a standard Windows handle, not one CBFilter allocated, so close it with the ordinary Win32 CloseHandle function rather than filter.CloseHandle, which is reserved for handles obtained from CBFilter methods such as GetOriginatorToken itself.

Remote and Impersonated Access

If the file is accessed over the network, the token you retrieve reflects the account under which the request was made, which depends on how the share is being accessed. Authenticated network access impersonates the remote user, so the token reflects that user's real identity; guest-mode access resolves to the local GUEST account; and administrative shares (C$, ADMIN$, and similar) resolve to LOCAL_SYSTEM. If your policy needs to treat these cases differently, for example, denying administrative-share access outright rather than evaluating it against your normal allow list, check for these cases explicitly rather than assuming every token represents an interactive end user.

Building a Persistent Policy

The example above evaluates the SID on every request. If the same restriction should apply even when your controller application is not running, and should survive until it is explicitly removed, look at default rules rather than re-implementing this check as a standing part of your application's startup logic; see Access and Default Rules in CBFS Filter for how AddDefaultRule differs from the standard, application-scoped filter rules used in the example here.

Getting Started with CBFilter

You can find an evaluation version of the SDK for your platform and programming language in the Download Center. After downloading and installing the SDK, you will receive a library, sample code, and comprehensive documentation on your system. The documentation includes a "Getting Started" section with programming instructions. Additionally, free technical support is available during the evaluation phase.

We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@callback.com.