Define Permissions
A permission refers to the set of rules that define what actions a user, or application, can or cannot perform on a particular resource. This concept is fundamental to the principle of least privilege, which advises that users or applications be given the minimum permissions necessary to complete their tasks, reducing the potential damage they can cause.
Familiarity with the C3 AI Type System API is crucial for accurately defining permission strings, as the name of the Type is typically included in these strings. This understanding is essential for ensuring correct and effective permission assignments within the C3 Agentic AI Platform.
You can identify the permissions a role requires by first listing the tasks the user persona of the role needs to perform. After you identify the tasks the role needs, you can then determine the permissions for each task by:
- Checking the Type System documentation
- Testing permissions
In most cases, the name of each permission also describes what the permission allows you to do with it.
Anatomy of a permission string
A permission describes access to a specific action or actions. You define a single permission as a string. The permission string is comprises four tokens. A : is the token deliminator that separates each token of a permissions string.
Permission string format:
"access:typeName:actionGroup:action"
Fields in a permission string:
access:allowordenytypeName: Type name or*for all TypesactionGroup: action group nameaction: action name or*for all actions
Examples:
"allow:*::*"– Allow access to all actions in all Types (ClusterAdmin)."deny:*:cluster-admin:"– Deny access foractionGroupcluster-adminfor all Types."allow:User::upsert"– AllowUser.upsert()access. This permits the user to perform an upsert.
You can specify only an actionGroup or an action. You cannot specify both on the same permission string.
Use ExpressionEngineFunction methods in a data permission
You can include an expression from the ExpressionEngineFunction Type in the condition field of a data permission string.
Here is an example of an ExpressionEngineFunction expression in a data permission string:
"dataPermissions": [
"Role::fetch:intersects('TestRole13', _context.accessControlEntities.id)"
]You can also format the data permission as the following:
"dataPermissions": [
{
"condition": "intersects('TestRole13', _context.accessControlEntities.id)",
"typeName": "Role",
"action": "fetch"
},
]This example provides the following data access:
Role: Refers to the Role Typefetch: Allows the user to retrieve data from the Typeintersects('TestRole13', _context.accessControlEntities.id): Uses theintersectsexpression from ExpressionEngineFunction.
The expression in this example checks for a value overlap between two collections:
TestRole13: A specific role_context.accessControlEntities.id: IDs of users, user groups, and roles associated with the user who is running the action. See the ActionCtxFilter and AccessControlEntity Types.
If the string TestRole13 is present in the access control entity IDs associated with the user, the expression evaluates to true and the user is able to fetch the role.
Access
A permission string can contain one or two access types, allow or deny.
Type name
The second token in a permission string is a Type. For example, the permission string below targets the Type WindTurbine.
"allow:WindTurbine::rebootEvents"
Action groups
By default, access is denied to every action.
Action groups are particularly useful when granting read access on a Persistable Type. For example, rather than specifying the type SmartBulb and every method, such as fetch and get, it is more efficient to include the action group read on the permission.
To retrieve a (non-unique) list of all action groups you can call Authorizer#actionGroups.
In rare cases when you need to create a custom action group, you can do so by annotating the specific method of the Type with an action annotation.
Action
The fourth token in a permission string is an action. Using the permission string, "allow:WindTurbine::rebootEvents", you can see the permission string permits the rebootEvents logic to be run.
Permissions for inner Type methods
To define permissions for an inner Type, append the inner Type name (.InnerType) or the wildcard notation (.*) to the Type name:
"allow:TypeName.InnerType::action"- Allow permissions to call a specific inner Type of a Type."allow:TypeName.*:*:"- Allow permissions to call every inner Type of a Type.
To learn more about inner Types, see Inner Types.
Type of permissions
Permissions define access control for a specific resource or pre-defined group of resources. The Permission Type provides access to an action, such as a method, or a group of actions, such as an action group.
DataPermissions
DataPermissions are metadata types used to define access control for specific resources. A dataPermission is an expression that defines a filter on a Permission for data access.
Anatomy of a data permission string
A data permission, DataPermission assigns an expression to a specific action or actions, such as a method. Any field from the Type can be used in the condition for evaluation. A data permission is a string. The data permission string is comprised of four tokens. Each token is separated by a token delimiter, :.
Data permission string format
"typeName:actionGroup:action:condition"
Fields in a data permission string
typeName: Type name or*for all TypesactionGroup: action group nameaction: action name or*for all actionscondition: expression to be evaluated by action
Examples
"User:write::(id == _context.userName)"- Write only the user's ownUserobject."User:read::(1 == 1)"- Read (fetch, get, etc) any User object"MemberAccount::evaluate:(member == _context.userName)"-MemberAccount.evaluateonly if theMemberAccount.memberfield equals the actionuserName.
You can only specify one actionGroup or action.
About the _context object
The _context object dynamically references the context of the permission string.
For example, "User:write::(id == _context.userName)" references the username of the user who is running the action, and the user can only perform the action if the id matches their username.
In the example "MemberAccount::evaluate:(member == _context.userName)", the user can only perform MemberAccount.evaluate if the member field matches their username.
In both of these examples, _context ensures users can only modify or fetch their own user data.
To see all fields you can use with the _context object, see the ActionCtxFilter Type.
Access to data
Data authorization refers to the process of granting or denying specific permissions to access and interact with data or resources based on a user's identity, role, or other attributes. It determines what actions, operations, or processes a user is allowed to perform on a data resource.
There are two different mechanisms that perform data authorization:
Expression-based : Expression-based data authorization is where access to data or resources is controlled by evaluating expressions rather than relying strictly on static roles or permissions. These expressions can combine various attributes of the user, resource, environment, and more, to make real-time decisions about access.
Access Control List or ACL-based
Additionally, secrets access for the Config related Types is strictly enforced based upon the user Role and ConfigOverride level according to the following rules:
ConfigOverride.CLUSTER: Secrets are only available to users with theClusterAdminrole.ConfigOverride.ENV: Secrets are available to users withEnvAdmin(or higher) roles.ConfigOverride.APP: Secrets are available to users withAppAdmin(or higher) roles.
The main purpose of ConfigOverride is to determine the visibility of the Config. For example, if the Config is set to ConfigOverride.Cluster, that means the Config is set to the cluster level, and all environments, applications, and users can view and access the cluster.
If a Config is set to ConfigOverride.User, then that means the Config is set to the user level, and only the user themselves for which the Config is set can view and access it.
It is because of the level of visibility that gives rise to the restrictions on who can set the Config for the given override. Cluster Admins can set configs at the Cluster level, and Env Admins for the Env level. Configs that are at a certain ConfigOverride can only be set by users with roles that are set at that override or greater.
Secrets, are slightly different. Secrets are able to be set and viewed by all users who have roles that are at or greater than the ConfigOverride. If the Config has ConfigOverride.Env, then the secrets can only be set and viewed by those who are an EnvAdmin or ClusterAdmin.
Expression-based
Expression-based data authorization is done through the DataPermission Type associated to Roles.
Currently, the only supported actionGroups for DataPermissions are for the following operations on Persistable Types:
- Create
- Read
- Update
- Remove
The action condition may refer to fields in the object itself, which can evaluate based on the original value of specific object instance being authorized.
ACL-based
ACL-based data authorization grants specific access privileges to members on individual object instances in the database. ACL data visibility is defined using the Types AclEntry, AclEnabled, EnableAclPrivilege and AclPrivliege. After a Type definition mixes AclEnabled, an EnableAclPrivilege object needs to be created for the Type that is AclEnabled and set the enabled flag to true to actually have ACL authorization enforced. This allows turning off ACL authorization to set up ACL entries and also allowing us to ship Types that are prepared to use ACL authorization but the choice to do so can be made in individual environments. If AclPrivilge entries are defined for the Type, then those privileges will automatically populate the ACL entries whenever their dependencies change. Note that this is limited to changes in dependencies within the current app itself (for example, no dependencies for apps accessed via data sharing will cause an automatic refresh).
The ACL data authorization handles the fetch and get authorization differently than the update and remove authorization. For fetch and get operations it will add a filter to the query to return only objects where there exists an AclEntry for a member in the member list of the current context user (User or Role members). For update and remove operations it will examine the ACL entries for the object being updated or removed and make sure there is at least one ACL entry that authorizes one of the members in the current user's member list (User and Roles) to perform the action. If none is found, the operation is denied. The creator of an AclEnabled object will always be granted access to it.
Note that when a FetchSpec#include traverses a reference to a type with ACL enabled, the ACL filter is applied and the referenced data is only returned if the caller is granted access to it. However, when filtering or ordering traverses a reference to a type with ACL enabled, by default, the ACL filter is not included in the filter expression. In order to block the ability to filter or order on ACL enabled references, see Ann.Db#blockAclRefFiltering.
Example 1
Assume there is a type Building with associated types Apartment, Fixture, and SmartBulb.
A role exist, Building1 Operator, and this particular role allows the user to access only data on Building1 and the corresponding Types associated with a Building, such as, Apartment, Fixture, and SmartBulb.
This role-permission setting is accomplished with the following example.
The example uses the permissions field to limit read access to Building1 (bld1), for the relationships between the Types: Apartment, Fixture, and SmartBulb, and Building -- all have a field building.
The dataPermission field specifies this Role is able to read Apartments, Fixtures, and SmartBulbs, where the value in the building field is bld1.
{
"id" : "BuildingA.Operator.Role",
"permissions" : [
"allow:Building:read:",
"allow:Apartment:read:",
"allow:Fixture:read:",
"allow:SmartBulb:read:"
],
"dataPermissions" : [
"Building:read::(building =='bld1')",
"Apartment:read::(building =='bld1')",
"Fixture:read::(building =='bld1')",
"SmartBulb:read::(building =='bld1')"
]
}Example 2
In the following example, a Building Manager role is defined that allows the user to access data on all buildings. The permissions allow users to read, or fetch, the Building, Apartment, Fixture and SmartBulb types. The following data permissions allow users to view all building, apartment, fixture, and smart bulb records.
{
"id" : "AllBuildings.Manager.Role",
"permissions" : [
"allow:Building:read:",
"allow:Apartment:read:",
"allow:Fixture:read:",
"allow:SmartBulb:read:"
],
"dataPermissions" : [
"Building:read::(1==1)",
"Apartment:read::(1==1)",
"Fixture:read::(1==1)",
"SmartBulb:read::(1==1)"
]
}Example 3
The following example creates a role that restricts the visibility to SmartBulbs manufactured by Philips, and only allows the role to fetch SmartBulb data:
{
"id" : "Philips.SmartBulbs.Role",
"permissions" : [
"allow:SmartBulb::fetch"
],
"dataPermissions" : [
"SmartBulb:read::(manufacturer=='Philips')"
]
}Example 4
The following example is the same as Example 3, however, there are no dataPermissions specified:
{
"id" : "Philips.SmartBulbs.Role",
"permissions" : [
"allow:SmartBulb::fetch"
]
}If no dataPermissions are specified, it is equivalent to specifying:
{
"dataPermissions" : [
"*::*:(FullDataAccess)"
]
}