Access Controls #

The storage engine has two features which control access to objects. Object ownership and access permissions.

Object ownership #

A storage object is created with an owner. The owner is either the user who created it, the system owner, or an owner assigned when the object is created with the code runtime.

When writing an object from the code runtime the owner is implied to be the system user unless explicitly set. A user who writes a storage object from a client is set as the owner by default.

System owned objects are created under the system user, represented in the server by a nil UUID (00000000-0000-0000-0000-000000000000). An object which is system owned must have public read access permissions before it can be fetched by clients.

These code examples show how to retrieve an object owned by the system (marked with public read).

Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
curl -X POST "http://127.0.0.1:7350/v2/storage" \
  -H 'Authorization: Bearer <session token>' \
  -d '{
    "object_ids": [
      {
        "collection": "configuration",
        "key": "config",
      }
    ]
  }'
Client
1
2
3
4
5
6
7
const objects = await client.readStorageObjects(session, {
  "object_ids": [{
    "collection": "configurations",
    "key": "config"
  }]
});
console.info("Read objects: %o", objects);
Client
1
2
3
4
5
6
var result = await client.ReadStorageObjectsAsync(session, new StorageObjectId {
    Collection = "configuration",
    Key = "config"
});

Console.WriteLine("Read objects: [{0}]", string.Join(",\n  ", result.Objects));
Client
1
2
3
let result = await client.readStorageObjects(session: session, ids: [StorageObjectId(collection: "configuration", key: "config")])

debugPrint("Read objects:", result.objects)
Client
1
2
3
4
5
6
7
final result = await client.readStorageObject(
  session: session,
  collection: 'configuration',
  key: 'config',
);

print('Read objects: ${result}');
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
auto successCallback = [](const NStorageObjects& objects)
{
  for (auto& object : objects)
    {
        std::cout << "Object key: " << object.key << ", value: " << object.value << std::endl;
    }
};

std::vector<NReadStorageObjectId> objectIds;
NReadStorageObjectId objectId;
objectId.collection = "configurations";
objectId.key = "config";
objectIds.push_back(objectId);
client->readStorageObjects(session, objectIds, successCallback);
Client
1
2
3
4
StorageObjectId objectId = new StorageObjectId("configuration");
objectId.setKey("config");
StorageObjects objects = client.readStorageObjects(session, objectId).get();
System.out.format("Read objects %s", objects.getObjectsList().toString());
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var result : NakamaAPI.ApiStorageObjects = yield(client.read_storage_objects_async(session, [
    NakamaStorageObjectId.new("configuration", "config")
]), "completed")

if result.is_exception():
    print("An error occurred: %s" % result)
    return

print("Read objects:")

for o in result.objects:
    print("%s" % o)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var result : NakamaAPI.ApiStorageObjects = await client.read_storage_objects_async(session, [
    NakamaStorageObjectId.new("configuration", "config")
])

if result.is_exception():
    print("An error occurred: %s" % result)
    return

print("Read objects:")

for o in result.objects:
    print("%s" % o)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
POST /v2/storage
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "object_ids": [
    {
      "collection": "configuration",
      "key": "config"
    }
  ]
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
local objects_ids = {
  {
    collection = "configuration",
    key = "config"
  }
}

local result = client.read_storage_objects(objects_ids)

if result.error then
  print(result.message)
  return
end

for _,object in ipairs(result.objects) do
  pprint(object)
end

You can also use the code runtime to fetch an object. The code runtime is exempt from the standard rules around access permissions because it is run by the server as authoritative code.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local object_ids = {
  { collection = "configuration", key = "config", user_id = nil },
}

local objects = nk.storage_read(object_ids)

for _, o in ipairs(objects) do
  local message = ("value: %q"):format(o.value)
  nk.logger_info(message)
end
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
objectIds := []*runtime.StorageRead{
    &runtime.StorageRead{
        Collection: "configuration",
        Key: "config",
    },
}

objects, err := nk.StorageRead(ctx, objectIds)

if err != nil {
    // Handle error.
} else {
    for _, object := range objects {
        logger.Info("value: %s", object.Value)
    }
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let objectIds: nkruntime.StorageReadRequest[] = [
  { collection: 'configuration', key: 'config', userId: '<uuid>' },
]

let objects: nkruntime.StorageObject[] = [];

try {
  let objects = nk.storageRead(objectIds);
} catch (error) {
  // Handle error
}

objects.forEach(function (o) {
  logger.info('value: %q', o.value);
});

Object permissions #

An object has permissions which are enforced for the owner of that object when writing or updating the object:

  • ReadPermission can have Public Read (2), Owner Read (1), or No Read (0).
  • WritePermission can have Owner Write (1), or No Write (0).

These permissions are ignored when interacting with the storage engine via the code runtime as the server is authoritative and can always read/write objects. As a result, No Read / No Write permissions mean that no client can read/write the object.

Objects with permission Owner Read and Owner Write may only be accessed or modified by the user who owns it. No other client may access the object.

Public Read means that any user can read that object. This is very useful for gameplay where users need to share their game state or parts of it with other users. For example you might have users with their own "Army" object who want to battle each other. Each user can write their own object with public read and it can be read by the other user so that it can be rendered on each others’ devices.

When modifying objects from the client, the default permission of a object is set to Owner Read and Owner Write. When modifying objects from the code runtime, the default permission of an object is set to No Read and No Write. When listing objects you’ll only get back objects with appropriate permissions.

Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# "2" refers to Public Read permission
# "1" refers to Owner Write permission
curl -X PUT "http://127.0.0.1:7350/v2/storage" \
  -H 'Authorization: Bearer <session token>' \
  -d '{
    "objects": [
      {
        "collection": "battle",
        "key": "army",
        "value": "{\"soldiers\": 50}",
        "permission_read": 2,
        "permission_write": 1
      }
    ]
  }'
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var army_setup = { "soldiers": 50 };
// "2" refers to Public Read permission
// "1" refers to Owner Write permission
const object_ids = await client.writeStorageObjects(session, [
  {
    "collection": "saves",
    "key": "savegame",
    "value": army_setup,
    "permission_read": 2,
    "permission_write": 1
  }
]);

console.info("Stored objects: %o", object_ids);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var armySetup = "{ \"soldiers\": 50 }";
// "2" refers to Public Read permission
// "1" refers to Owner Write permission
var result = await client.WriteStorageObjectsAsync(session, new WriteStorageObject
{
    Collection = "saves",
    Key = "savegame",
    Value = armySetup,
    PermissionRead = 2,
    PermissionWrite = 1
});

Console.WriteLine("Stored objects: [{0}]", string.Join(",\n  ", result.Objects));
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let armySetup = "{ \"soldiers\": 50 }"
let objects = [WriteStorageObject(
    collection: "saves",
    key: "savegame",
    value: armySetup,
    permissionRead: .publicRead,
    permissionWrite: .ownerWrite
)]
let result = try await client.writeStorageObjects(session: session, objects: objects)

debugPrint("Stored objects:", result.acks)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
final armySetup = '{"soldiers": 50}';
final result = await client.writeStorageObject(
  session: session,
  collection: 'saves',
  key: 'savegame',
  value: armySetup,
  writePermission: StorageWritePermission.ownerWrite,
  readPermission: StorageReadPermission.publicRead,
);

print('Stored objects: ${result.acks}');
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
auto successCallback = [](const NStorageObjectAcks& acks)
{
};

std::vector<NStorageObjectWrite> objects;
NStorageObjectWrite object;
object.collection = "saves";
object.key = "savegame";
object.value = "{ \"soldiers\": 50 }";
object.permissionRead = NStoragePermissionRead::PUBLIC_READ;   // Public Read permission
object.permissionWrite = NStoragePermissionWrite::OWNER_WRITE; // Owner Write permission
objects.push_back(object);
client->writeStorageObjects(session, objects, successCallback);
Client
1
2
3
4
String armySetup = "{ \"soldiers\": 50 }";
StorageObjectWrite object = new StorageObjectWrite("saves", "savegame", armySetup, PermissionRead.PUBLIC_READ, PermissionWrite.OWNER_WRITE);
StorageObjectAcks acks = client.writeStorageObjects(session, object).get();
System.out.format("Stored objects %s", acks.getAcksList());
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var army_setup = "{ \"soldiers\": 50 }";
# "2" refers to Public Read permission
# "1" refers to Owner Write permission
var acks : NakamaAPI.ApiStorageObjectAcks = yield(client.write_storage_objects_async(session, [
    NakamaWriteStorageObject.new("saves", "savegame", 2, 1, army_setup, "")
]), "completed")

if acks.is_exception():
    print("An error occurred: %s" % acks)
    return

print("Stored objects: %s" % [acks.acks])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var army_setup = "{ \"soldiers\": 50 }";
# "2" refers to Public Read permission
# "1" refers to Owner Write permission
var acks : NakamaAPI.ApiStorageObjectAcks = await client.write_storage_objects_async(session, [
    NakamaWriteStorageObject.new("saves", "savegame", 2, 1, army_setup, "")
])

if acks.is_exception():
    print("An error occurred: %s" % acks)
    return

print("Stored objects: %s" % [acks.acks])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
PUT /v2/storage
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "objects": [
    {
      "collection": "battle",
      "key": "army",
      "value": "{ \"soldiers\": 50 }",
      "permission_read": 2,
      "permission_write": 1
    }
  ]
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
local army_setup = json.encode({ soldiers = 50 })
local can_read = 2
local can_write = 1

local objects = {
  {
    collection = "battle",
    key = "army",
    permissionRead = can_read,
    permissionWrite = can_write,
    value = army_setup,
    version = version
  }
}

local result = client.write_storage_objects(objects)

if result.error then
  print(result.message)
  return
end

for _,ack in ipairs(result.acks) do
  pprint(ack)
end

You can store an object with custom permissions from the code runtime.

Code snippet for this language TypeScript has not been found. Please choose another language to show equivalent examples.
Server
1
2
3
4
5
6
7
local user_id = "4ec4f126-3f9d-11e7-84ef-b7c182b36521" -- Some user ID.

local new_objects = {
  { collection = "battle", key = "army", user_id = user_id, value = {}, permission_read = 2, permission_write = 1 }
}

nk.storage_write(new_objects)
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
userID := "4ec4f126-3f9d-11e7-84ef-b7c182b36521" // Some user ID.
objects := []*runtime.StorageWrite{
    &runtime.StorageWrite{
        Collection:      "battle",
        Key:             "army",
        UserID:          userID,
        Value:           "{}",
        PermissionRead:  2,
        PermissionWrite: 1,
    },
}

if _, err := nk.StorageWrite(ctx, objects); err != nil {
    // Handle error.
}

Related Pages