Add group to filesystem using the api

Question: What is the JSON syntax using API to add a group to filesystem acl?

Step 1
Show the current ACL of WinShare1

PS C:\Users\david> $AclProperties = @{
  path = $datasetMntPath 
  simplified = $true
  resolve_ids = $false
}
$AclPropertiesJson = $AclProperties | ConvertTo-Json 
$AclPropertiesJson

$AclPropertiesResponse = Invoke-TrueNASAPI -method Post -url "filesystem/getacl" -body $AclPropertiesJson
$AclPropertiesResponse
$AclPropertiesResponse.acl | Format-Table -AutoSize

{
    "simplified":  true,
    "path":  "/mnt/testzpool1/WinShare1",
    "resolve_ids":  false
}


acl         : {@{tag=owner@; id=-1; perms=; flags=; type=ALLOW}, @{tag=group@; id=-1; perms=; flags=; type=ALLOW}, @{tag=GROUP; id=545; perms=; flags=; type=ALLOW}, @{tag=GROUP; id=544; perms=; flags=; type=ALLOW}}
trivial     : False
uid         : 0
gid         : 0
path        : /mnt/testzpool1/WinShare1
nfs41_flags : @{protected=False; defaulted=False; autoinherit=False}
acltype     : NFS4




tag     id perms                 flags            type 
---     -- -----                 -----            ---- 
owner@  -1 @{BASIC=FULL_CONTROL} @{BASIC=INHERIT} ALLOW
group@  -1 @{BASIC=MODIFY}       @{BASIC=INHERIT} ALLOW
GROUP  545 @{BASIC=MODIFY}       @{BASIC=INHERIT} ALLOW
GROUP  544 @{BASIC=FULL_CONTROL} @{BASIC=INHERIT} ALLOW

Step 2
manaully removed the two built-in groups (545 and 555)
and add group 3000

results

PS C:\Users\david> $AclProperties = @{
  path = $datasetMntPath 
  simplified = $true
  resolve_ids = $false
}
$AclPropertiesJson = $AclProperties | ConvertTo-Json 
$AclPropertiesJson

$AclPropertiesResponse = Invoke-TrueNASAPI -method Post -url "filesystem/getacl" -body $AclPropertiesJson
$AclPropertiesResponse
$AclPropertiesResponse.acl | Format-Table -AutoSize

{
    "simplified":  true,
    "path":  "/mnt/testzpool1/WinShare1",
    "resolve_ids":  false
}


acl         : {@{tag=owner@; id=-1; perms=; flags=; type=ALLOW}, @{tag=group@; id=-1; perms=; flags=; type=ALLOW}, @{tag=GROUP; id=3000; perms=; flags=; type=ALLOW}}
trivial     : False
uid         : 0
gid         : 0
path        : /mnt/testzpool1/WinShare1
nfs41_flags : @{protected=False; defaulted=False; autoinherit=False}
acltype     : NFS4




tag      id perms                 flags            type 
---      -- -----                 -----            ---- 
owner@   -1 @{BASIC=FULL_CONTROL} @{BASIC=INHERIT} ALLOW
group@   -1 @{BASIC=MODIFY}       @{BASIC=INHERIT} ALLOW
GROUP  3000 @{BASIC=FULL_CONTROL} @{BASIC=INHERIT} ALLOW

Step 3
manually remove group 3000
(skipping output)

Step 4
add group 3000 using the api (failed)

error
error : [EINVAL] filesystem_acl.dacl: Result does not match specified schema: [EINVAL] nfs4_acl: Not a list
[EINVAL] posix1e_acl: Not a list

PS C:\Users\david> $AclPayload =@{
    "path" = "/mnt/testzpool1/WinShare1"
    "uid" = $null
    "gid" = $null
    "dacl" = @{
        "nfs4_acl" = @(
            @{
                "type" = "ALLOW"
                "flags" = "FULL_CONTROL"
                "who" = "GROUP@3000"
            }
        )
        "posix1e_acl" = @()  # Ensure posix1e_acl is an empty list
    }
    "acltype" = "NFS4"
}





$AclPayloadJson = $AclPayload | ConvertTo-Json  -Depth 100
$AclPayloadJson

$AclSetResponseID = Invoke-TrueNASAPI -method Post -url "filesystem/setacl" -body $AclPayloadJson
$AclSetResponseID

$joburl = "core/get_jobs/?id=" + $AclSetResponseID
Invoke-TrueNASAPI -method Get -url $jobUrl
{
    "dacl":  {
                 "posix1e_acl":  [

                                 ],
                 "nfs4_acl":  [
                                  {
                                      "flags":  "FULL_CONTROL",
                                      "who":  "GROUP@3000",
                                      "type":  "ALLOW"
                                  }
                              ]
             },
    "path":  "/mnt/testzpool1/WinShare1",
    "acltype":  "NFS4",
    "gid":  null,
    "uid":  null
}
3143


id            : 3143
method        : filesystem.setacl
arguments     : {@{dacl=; path=/mnt/testzpool1/WinShare1; acltype=NFS4; gid=; uid=}}
transient     : False
description   : 
abortable     : False
logs_path     : 
logs_excerpt  : 
progress      : @{percent=0; description=; extra=}
result        : 
error         : [EINVAL] filesystem_acl.dacl: Result does not match specified schema: [EINVAL] nfs4_acl: Not a list
                [EINVAL] posix1e_acl: Not a list
                
                
exception     : Traceback (most recent call last):
                  File "/usr/lib/python3/dist-packages/middlewared/job.py", line 469, in run
                    await self.future
                  File "/usr/lib/python3/dist-packages/middlewared/job.py", line 513, in __run_body
                    rv = await self.middleware.run_in_thread(self.method, *args)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  File "/usr/lib/python3/dist-packages/middlewared/main.py", line 1324, in run_in_thread
                    return await self.run_in_executor(self.thread_pool_executor, method, *args, **kwargs)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  File "/usr/lib/python3/dist-packages/middlewared/main.py", line 1321, in run_in_executor
                    return await loop.run_in_executor(pool, functools.partial(method, *args, **kwargs))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  File "/usr/lib/python3.11/concurrent/futures/thread.py", line 58, in run
                    result = self.fn(*self.args, **self.kwargs)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  File "/usr/lib/python3/dist-packages/middlewared/schema/processor.py", line 190, in nf
                    args, kwargs = clean_and_validate_args(args, kwargs)
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  File "/usr/lib/python3/dist-packages/middlewared/schema/processor.py", line 180, in clean_and_validate_args
                    verrors.check()
                  File "/usr/lib/python3/dist-packages/middlewared/service_exception.py", line 70, in check
                    raise self
                middlewared.service_exception.ValidationErrors: [EINVAL] filesystem_acl.dacl: Result does not match specified schema: [EINVAL] nfs4_acl: Not a list
                [EINVAL] posix1e_acl: Not a list
                
                
                
exc_info      : @{repr=ValidationErrors(); type=VALIDATION; extra=System.Object[]}
state         : FAILED
time_started  : @{$date=1713152506000}
time_finished : @{$date=1713152506000}
credentials   : @{type=API_KEY; data=}

The filesystem ACL API is not CRUD. You need to submit the ACL in its entirety. You can look at our API tests for reference. middleware/tests/api2/test_345_acl_nfs4.py at master · truenas/middleware · GitHub

Here’s what I came up with that seems to work


function Invoke-TrueNASAPI {
  param (
    [string] $method,
    [string] $url,
    [object] $body = $null
  )



  $apiKey = "{YourTruenasApiKey}"
  $apiUrl = "https://{YourTruenasIP}/api/v2.0"




  $headers = New-Object System.Net.WebHeaderCollection
  $headers = @{
    accept="*/*"
    Authorization="Bearer $apiKey"
}

 
add-type @"
   using System.Net;
   using System.Security.Cryptography.X509Certificates;
   public class TrustAllCertsPolicy : ICertificatePolicy {
      public bool CheckValidationResult(
      ServicePoint srvPoint, X509Certificate certificate,
      WebRequest request, int certificateProblem) {
      return true;
   }
}
"@
  [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

  $contentType = "application/json"
  
  
  ###commented out this and the dataset is created
  #if ($body) {
  #  $body = ConvertTo-Json $body
  #}

  Invoke-RestMethod -Uri "$apiUrl/$url" -Method $method -Headers $headers -UseBasicParsing -ContentType $contentType -Body $body
}




$datasetMntPath = "/mnt/testzpool1/WinShare1"

$AclPayload = @{
    dacl = @(
        @{
            tag="owner@"
            id=-1
            perms=@{BASIC="FULL_CONTROL"}
            flags=@{BASIC="INHERIT"}
            type="ALLOW"
        },
        @{
            tag="group@"
            id=-1
            perms=@{BASIC="MODIFY"}
            flags=@{BASIC="INHERIT"}
            type="ALLOW"
        },
        @{
            tag="GROUP"
            id=3000
            perms=@{BASIC="FULL_CONTROL"}
            flags=@{BASIC="INHERIT"}
            type="ALLOW"
        }
    )
    uid = 0
    gid = 0
    path = $datasetMntPath 
    nfs41_flags = @{
        protected = $false
        defaulted = $false
        autoinherit = $false
    }
    acltype = "NFS4"
}




$AclPayloadJson = $AclPayload | ConvertTo-Json  -Depth 100
$AclPayloadJson

$AclSetResponseID = Invoke-TrueNASAPI -method Post -url "filesystem/setacl" -body $AclPayloadJson
$AclSetResponseID

$joburl = "core/get_jobs/?id=" + $AclSetResponseID
Invoke-TrueNASAPI -method Get -url $jobUrl


These are long-running jobs and so you’ll need to put some thought into waiting for them to complete. If you are recursively changing permissions on a million files it may take some time.

good point

do {
$GetState = Invoke-TrueNASAPI -method Get -url $jobUrl
    $GetState
    Start-Sleep -Seconds 15
}  while ( $GetState.State -eq "RUNNING"  )

A tight loop polling that interface is a bad idea. Put some sleeps in it. Honestly, if you’re familiar with powershell a better and more efficient solution would be to use the websocket API rather than REST.

updated : added Start-Sleep -Seconds 15

if you’re familiar with powershell a better and more efficient solution would be to use the websocket API rather than REST.
Good to know. I appreciate the feedback. I’ll look into it.

If you do this then you’ll be able to subscribe for updates to that particular job as opposed to polling constantly. Performance will generally be better as well.

You can also potentially use our python middleware client directly in Windows. This would require installing python on Windows and installing websocket-client via pip.

Our websocket client is here: middleware/src/middlewared/middlewared/client at master · truenas/middleware · GitHub

>>> from middlewared.client import Client
>>> c = Client("ws://192.168.0.225/websocket")
>>> c.call("auth.login", "root", "Cats")
True
>>> c.call('smb.config')
{'id': 1, 'netbiosname': 'truenas', 'netbiosalias': [], 'workgroup': 'WORKGROUP', 'description': 'TrueNAS Server', 'unixcharset': 'UTF-8', 'loglevel': 'MINIMUM', 'syslog': False, 'aapl_extensions': False, 'localmaster': True, 'guest': 'nobody', 'filemask': '', 'dirmask': '', 'smb_options': '', 'bindip': [], 'cifs_SID': 'S-1-5-21-1387515775-1169731188-2675585453', 'ntlmv1_auth': False, 'enable_smb1': False, 'admin_group': None, 'next_rid': 0, 'multichannel': False, 'netbiosname_local': 'truenas'}
>>> c.call('core.get_jobs', [], {'limit': 1})
[{'id': 11, 'method': 'disk.sync_all', 'arguments': [], 'transient': False, 'description': None, 'abortable': False, 'logs_path': None, 'logs_excerpt': None, 'progress': {'percent': 100, 'description': 'Syncing all disks complete', 'extra': None}, 'result': 'OK', 'error': None, 'exception': None, 'exc_info': None, 'state': 'SUCCESS', 'time_started': datetime.datetime(2024, 4, 8, 0, 32, 29, tzinfo=datetime.timezone.utc), 'time_finished': datetime.datetime(2024, 4, 8, 0, 32, 29, tzinfo=datetime.timezone.utc), 'credentials': {'type': 'UNIX_SOCKET', 'data': {'username': 'root'}}}]

trivial example of some things

1 Like