Kodall
Data Modeling

The Access Control List

Enabling ACL on Entities

When you enable Access Control Lists (ACL) for an entity within your customization set, the system automatically generates a dedicated ACL entity to handle its permissions. This new entity is named by appending the _acl suffix to the original entity's name.

For example, enabling ACL on the storage_file entity creates a corresponding storage_file_acl entity.

How ACL Enhances Standard Security

It is important to note that ACL operates on top of your application's normal security model to provide a much finer level of control:

  • tandard Security: Typically governs broad, table-level access based on roles (e.g., "Does this user have the general right to view storage files?").
  • ACL Security: Governs granular, row-level access. It dictates exact permissions for a single, specific entry in the database (e.g., "Does this user have permission to view, update, or delete exactly File A?").

Managing Permissions and Data Structure

You can assign and manage these row-level permissions flexibly: either manually through the UI or programmatically via code.

Below is the generated data structure for the resulting ACL entity, showing how it maps the specific row (via foreign key) to the user, business unit, or organization, alongside their specific permissions:

Entity diagram:

Rendering diagram...

Best pratices

Workflow

ONE Workflow
// Ensures that an ACL (Access Control List) record exists for a storage file.
// This method is idempotent: it will insert a new record if one doesn't exist, 
// or update the existing one if it does, preventing duplicate entries.
method ensureStorageFileACL(storageFileKey as int, userKey as int, businessUnitKey as int, organizationKey as int, u as bool, r as bool, d as bool) {
    
    // --- 1. VALIDATION PHASE ---
    // A storage file key is always mandatory.
    validation->failIf(storageFileKey == null, "Argument 'storageFileKey' is required");
    
    // Validates that at least ONE target entity (User, Business Unit, or Organization) is provided.
    validation->failIf(userKey == null and businessUnitKey == null and organizationKey == null, "One argument is reqruied between 'userKey', 'businessUnitKey', 'organizationKey'");
    
    // Validates that EXACTLY ONE target entity is provided (preventing mixed ACL records).
    // Note: The variable is named 'isOnlyOne', but the logic actually checks if MORE THAN ONE is provided.
    var isOnlyOne = (userKey != null and businessUnitKey != null) or (userKey != null and organizationKey != null) or (businessUnitKey != null and organizationKey != null);
    validation->failIf(isOnlyOne, "You can use only one argument between 'userKey', 'businessUnitKey', 'organizationKey'");
    
    // --- 2. IDEMPOTENT UPSERT PHASE ---
    // Prepare a new, empty ACL record in memory. 
    var acl = CREATE storage_file_acl;

    // Handle User ACL
    if (userKey != null) {
        // Query the database to see if an ACL record already exists for this exact file and user.
        var exists = FETCH storage_file_acl(key) FILTER AND (id_storage_file == ${storageFileKey}, id_user_acl == ${userKey}) LIMIT 1**;
        
        // If a record is found, discard the newly created empty record and fetch the existing one.
        if (exists != null) {
            acl = GET storage_file_acl (exists.key);
        }
        
        // Pass the record (either brand new or fetched from DB) to the helper method to apply values.
        this->storageFileACL(acl, storageFileKey, userKey, businessUnitKey, organizationKey, u, r, d);
        return null;
    }
    
    // Handle Business Unit ACL (Same idempotent logic as User)
    if (businessUnitKey != null) {
        var exists = FETCH storage_file_acl(key) FILTER AND (id_storage_file == ${storageFileKey}, id_business_unit_acl == ${businessUnitKey}) LIMIT 1**;
        if (exists != null) {
            acl = GET storage_file_acl (exists.key);
        }
        this->storageFileACL(acl, storageFileKey, userKey, businessUnitKey, organizationKey, u, r, d);
        return null;
    }
    
    // Handle Organization ACL (Same idempotent logic as User)
    if (organizationKey != null) {
        var exists = FETCH storage_file_acl(key) FILTER AND (id_storage_file == ${storageFileKey}, id_organization_acl == ${organizationKey}) LIMIT 1**;
        if (exists != null) {
            acl = GET storage_file_acl (exists.key);
        }
        this->storageFileACL(acl, storageFileKey, userKey, businessUnitKey, organizationKey, u, r, d);
        return null;
    }
}

// Helper method to map the provided parameters to the database entity fields.
method storageFileACL(acl as ENTITY storage_file_acl, storageFileKey as int, userKey as int, businessUnitKey as int, organizationKey as int, u as bool, r as bool, d as bool) {
    
    // Map the foreign keys
    acl.id_storage_file = storageFileKey;
    acl.id_user_acl = userKey;
    acl.id_business_unit_acl = businessUnitKey;
    acl.id_organization_acl = organizationKey;
    
    // Map the boolean permission flags (Read, Update, Delete)
    acl.entity_get = r;
    acl.entity_update = u;
    acl.entity_delete = d;

    // Save the record to the database.
    // Because of the logic in the parent method, this will act as an INSERT 
    // if 'acl' is a new entity, or an UPDATE if 'acl' was fetched from the database.
    PUT acl;
}
Operation on ACL entityAllow ifScope of operationOwnershipScope of access
Get1. The parent can be read - granted via entity_security, or
2. The parent can be read - granted via ACL
All ACLs of parent entities for which the get privilege is granted.N/AN/A
CreateThe parent entity can be updated - defined via entity_securityInherits scope of the create for parent entity.Owner1. Get - if the user can read the parent entity
2. Update - if the user can update the parent entity
3. Delete - if the user can delete the parent entity
UpdateInherits scope of the update for parent entity.
DeleteInherits scope of the update for parent entity.N/AN/A

Security of the ACL entity

entity_get

If the parent entity can be read then all ACLs on the parent entity can be read. The read privilege on the parent entity can also be granted through the ACL.

entity_create

If the parent entity can be updated then ACLs can be created.

Ownership of the ACL entities will always be set to user that created them.

entity_update

If the parent entity can be updated then ACLs can be updated. The scope of the update will be shared with the scope of the update for parent entity.

Ownership of the ACL entities will always be set to user that updated them.

If update privilege is granted on the parent entity through the ACL then the update privilege will NOT be granted on the ACL entities.

entity_delete

If the parent entity can be updated then ACLs can be deleted. The scope of the delete will be shared with the scope of the update for parent entity.

If update privilege is granted on the parent entity through the ACL then the update privilege will NOT be granted on the ACL entities.

Scope of access

entity_get

Can be set if the user can read the parent entity.

entity_update

Can be set if the user can update the parent entity.

entity_delete

Can be set if the user can delete the parent entity.