Skip to content

Access Groups

Access groups provide fine-grained permission control for Total CMS, allowing you to restrict what users can access and modify in both the admin dashboard and via the REST API.

Access groups work at two levels:

  1. Middleware Enforcement: Routes are protected by middleware that checks permissions before allowing access
  2. UI Controls: Template helper functions hide/show UI elements based on permissions

Super admin users (members of the admin group in the default auth collection) bypass all access checks automatically.

Access groups are defined in tcms-data/.system/access-groups.json:

{
"groups": [
{
"id": "editor",
"description": "Blog and news editor",
"permissions": {
"collectionsMeta": {
"operations": ["read"],
"all": false,
"allowed": ["blog", "news"]
},
"collections": {
"operations": ["create", "read", "update", "delete"],
"all": false,
"allowed": ["blog", "news"]
},
"schemas": {
"operations": ["read"],
"all": false,
"allowed": ["blog", "news"]
},
"templates": true,
"mailer": false,
"playground": true,
"docs": true,
"utils": {
"all": false,
"allowed": ["cache-manager"]
},
"settings": {
"all": false,
"allowed": []
}
}
}
]
}

Total CMS uses CRUD (Create, Read, Update, Delete) operations for fine-grained permission control:

  • create - Create new resources/objects
  • read - View/list/fetch resources
  • update - Modify existing resources
  • delete - Delete/remove resources

These operations replace the older HTTP method approach and provide clearer, more intuitive permission control.

Collections have two separate permission levels:

Controls access to managing collection definitions (creating/editing/deleting collections themselves):

"collectionsMeta": {
"operations": ["read", "update"],
"all": false,
"allowed": ["blog", "news"]
}
  • create - Create new collections
  • read - View collection definitions
  • update - Edit collection settings
  • delete - Delete collections

Controls access to objects within collections:

"collections": {
"operations": ["create", "read", "update", "delete"],
"all": true,
"allowed": []
}
  • create - Create new objects
  • read - View/list objects
  • update - Edit objects
  • delete - Delete objects
  • all - If true, access all collections; if false, only those in allowed
  • allowed - Array of specific collection IDs
"schemas": {
"operations": ["create", "read", "update", "delete"],
"all": false,
"allowed": ["blog", "product"]
}
  • create - Create new schemas
  • read - View schemas
  • update - Edit schemas
  • delete - Delete schemas
  • all - If true, access all schemas; if false, only those in allowed
  • allowed - Array of specific schema names
"templates": true // or false

Simple boolean for full access or no access to templates. Templates don’t have granular CRUD permissions.

"settings": {
"all": false,
"allowed": ["general", "cache"]
}
  • all - If true, access all settings sections
  • allowed - Array of specific setting section names (e.g., “general”, “cache”, “auth”, “mailer”)
"utils": {
"all": false,
"allowed": ["cache-manager", "jumpstart"]
}
  • all - If true, access all utility pages
  • allowed - Array of specific utility page names

Simple true/false for features without granular control:

  • mailer - Access to mailer/email functionality
  • playground - Access to Twig playground
  • docs - Access to documentation

Total CMS provides helper functions to check permissions in your templates, allowing you to hide/show UI elements based on the current user’s access groups.

Check specific collection object access:

{% if cms.canAccessCollection('blog', 'read') %}
<a href="/admin/collections/blog">View Blog Posts</a>
{% endif %}
{% if cms.canAccessCollection('blog', 'create') %}
<a href="/admin/collections/blog/new">New Post</a>
{% endif %}
{% if cms.canAccessCollection('blog', 'update') %}
<button>Edit Post</button>
{% endif %}
{% if cms.canAccessCollection('blog', 'delete') %}
<button class="delete">Delete Post</button>
{% endif %}

Check general collection object access:

{% if cms.canAccessCollectionsOperation('read') %}
<p>You can view collections</p>
{% endif %}

Check collection metadata access:

{% if cms.canAccessCollectionMeta('blog', 'read') %}
<a href="/admin/collections/blog/settings">View Collection Settings</a>
{% endif %}
{% if cms.canAccessCollectionMeta('blog', 'update') %}
<button>Edit Collection Settings</button>
{% endif %}
{% if cms.canAccessCollectionMeta('blog', 'delete') %}
<button class="delete">Delete Collection</button>
{% endif %}

Check general collection metadata access:

{% if cms.canAccessCollectionsMetaOperation('read') %}
<a href="/admin/collections">View Collections List</a>
{% endif %}
{% if cms.canAccessCollectionsMetaOperation('create') %}
<a href="/admin/collections/new">New Collection</a>
{% endif %}

Get list of accessible collections:

{% for collection in cms.getAccessibleCollections('read') %}
<li>{{ collection }}</li>
{% endfor %}

Check specific schema access:

{% if cms.canAccessSchema('blog', 'read') %}
<a href="/admin/schemas/blog">View Blog Schema</a>
{% endif %}
{% if cms.canAccessSchema('blog', 'update') %}
<a href="/admin/schemas/blog/edit">Edit Schema</a>
{% endif %}
{% if cms.canAccessSchema('blog', 'delete') %}
<button class="delete">Delete Schema</button>
{% endif %}

Check general schemas access:

{% if cms.canAccessSchemasOperation('read') %}
<a href="/admin/schemas">View Schemas</a>
{% endif %}
{% if cms.canAccessSchemasOperation('create') %}
<a href="/admin/schemas/new">New Schema</a>
{% endif %}

Check templates access (boolean):

{% if cms.canAccessTemplates() %}
<a href="/admin/templates">Templates</a>
{% endif %}

Check specific utils page:

{% if cms.canAccessUtil('cache-manager') %}
<a href="/admin/utils/cache-manager">Cache Manager</a>
{% endif %}
{% if cms.canAccessUtil('jumpstart') %}
<a href="/admin/utils/jumpstart">JumpStart</a>
{% endif %}

Check general utils access:

{% if cms.canAccessUtils() %}
<a href="/admin/utils">Utils</a>
{% endif %}

Check mailer access:

{% if cms.canAccessMailer() %}
<a href="/admin/mailer">Mailer</a>
{% endif %}

Check playground access:

{% if cms.canAccessPlayground() %}
<a href="/admin/utils/twig-playground">Twig Playground</a>
{% endif %}

Check docs access:

{% if cms.canAccessDocs() %}
<a href="/admin/docs">Documentation</a>
{% endif %}

Check if user is super admin:

{% if cms.isAdmin() %}
<div class="admin-only-feature">
<a href="/admin/utils/access-groups">Manage Access Groups</a>
</div>
{% endif %}

Super admins bypass all access checks and have full access to everything.

<nav>
{% if cms.canAccessCollectionsMetaOperation('read') %}
<a href="/admin/collections">Collections</a>
{% endif %}
{% if cms.canAccessSchemasOperation('read') %}
<a href="/admin/schemas">Schemas</a>
{% endif %}
{% if cms.canAccessTemplates() %}
<a href="/admin/templates">Templates</a>
{% endif %}
{% if cms.canAccessUtils() %}
<a href="/admin/utils">Utils</a>
{% endif %}
{% if cms.isAdmin() %}
<a href="/admin/utils/access-groups">Access Groups</a>
{% endif %}
</nav>
<ul>
{% for collection in collections %}
{% if cms.canAccessCollection(collection.id, 'read') %}
<li>
<a href="/admin/collections/{{ collection.id }}">{{ collection.name }}</a>
{% if cms.canAccessCollection(collection.id, 'create') %}
<a href="/admin/collections/{{ collection.id }}/new">New Item</a>
{% endif %}
{% if cms.canAccessCollectionMeta(collection.id, 'update') %}
<a href="/admin/collections/{{ collection.id }}/settings">Settings</a>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
<div class="object-actions">
{% if cms.canAccessCollection(collection, 'create') %}
<a href="/admin/collections/{{ collection }}/new" class="btn">New Object</a>
{% endif %}
{% if cms.canAccessCollection(collection, 'update') %}
<button class="btn" data-action="edit">Edit</button>
{% endif %}
{% if cms.canAccessCollection(collection, 'delete') %}
<button class="btn btn-danger" data-action="delete">Delete</button>
{% endif %}
</div>
<div class="schema-actions">
{% if cms.canAccessSchemasOperation('create') %}
<a href="/admin/schemas/new" class="btn">New Schema</a>
{% endif %}
{% if cms.canAccessSchema(schema.id, 'update') %}
<a href="/admin/schemas/{{ schema.id }}/edit" class="btn">Edit</a>
{% endif %}
{% if cms.canAccessSchema(schema.id, 'delete') %}
<button class="btn btn-danger" data-schema="{{ schema.id }}">Delete</button>
{% endif %}
</div>
{
"id": "admin",
"description": "Full administrative access",
"permissions": {
"collectionsMeta": {
"operations": ["create", "read", "update", "delete"],
"all": true,
"allowed": []
},
"collections": {
"operations": ["create", "read", "update", "delete"],
"all": true,
"allowed": []
},
"schemas": {
"operations": ["create", "read", "update", "delete"],
"all": true,
"allowed": []
},
"templates": true,
"mailer": true,
"playground": true,
"docs": true,
"utils": {
"all": true,
"allowed": []
},
"settings": {
"all": true,
"allowed": []
}
}
}
{
"id": "editor",
"description": "Full CRUD access to specific collections",
"permissions": {
"collectionsMeta": {
"operations": ["read"],
"all": false,
"allowed": ["blog", "news"]
},
"collections": {
"operations": ["create", "read", "update", "delete"],
"all": false,
"allowed": ["blog", "news"]
},
"schemas": {
"operations": ["read"],
"all": false,
"allowed": ["blog", "news"]
},
"templates": false,
"mailer": false,
"playground": true,
"docs": true,
"utils": {
"all": false,
"allowed": []
},
"settings": {
"all": false,
"allowed": []
}
}
}
{
"id": "viewer",
"description": "Read-only access to all collections",
"permissions": {
"collectionsMeta": {
"operations": ["read"],
"all": true,
"allowed": []
},
"collections": {
"operations": ["read"],
"all": true,
"allowed": []
},
"schemas": {
"operations": ["read"],
"all": true,
"allowed": []
},
"templates": true,
"mailer": false,
"playground": true,
"docs": true,
"utils": {
"all": false,
"allowed": []
},
"settings": {
"all": false,
"allowed": []
}
}
}
{
"id": "blogger",
"description": "Full CRUD access to blog collection only",
"permissions": {
"collectionsMeta": {
"operations": ["read"],
"all": false,
"allowed": ["blog"]
},
"collections": {
"operations": ["create", "read", "update", "delete"],
"all": false,
"allowed": ["blog"]
},
"schemas": {
"operations": ["read"],
"all": false,
"allowed": ["blog"]
},
"templates": false,
"mailer": false,
"playground": true,
"docs": true,
"utils": {
"all": false,
"allowed": []
},
"settings": {
"all": false,
"allowed": []
}
}
}
  1. Use UI Controls Everywhere: Always check permissions before showing links, buttons, or forms. Don’t rely on middleware alone - good UX means hiding what users can’t access.

  2. Check Appropriate Operations: Use the appropriate CRUD operation for the action:

    • read for viewing/listing
    • create for creating new resources
    • update for modifying existing resources
    • delete for deleting resources
  3. Distinguish Collections from Collection Metadata:

    • Use canAccessCollection() for working with objects within a collection
    • Use canAccessCollectionMeta() for managing collection definitions
  4. Graceful Degradation: Hide buttons/actions users can’t perform rather than showing disabled buttons.

  5. Admin-Only Features: Use cms.isAdmin() for features that should never be delegated (like managing access groups, API keys, or user accounts).

  6. Operation-Level Checks: Use general operation checks (e.g., canAccessCollectionsOperation()) for listing pages where no specific resource is selected yet.

  7. Principle of Least Privilege: Grant users only the minimum permissions they need to accomplish their tasks.

Some routes require super admin access and cannot be delegated via access groups:

  • Access Groups Management: /admin/utils/access-groups
  • API Key Management: /admin/apikeys/*
  • User Management: /admin/users/*

These routes use AdminOnlyMiddleware which only allows super admin users.

OperationPurposeExample Use Case
createCreate new resourcesAdd new blog post, create collection
readView/list resourcesView blog posts, list schemas
updateModify existing resourcesEdit blog post, update collection settings
deleteRemove resourcesDelete blog post, remove collection