Best practices for designing an authorization model
As you prepare to use the Amazon Verified Permissions service within a software application, it can be challenging to leap immediately into writing policy statements as a first step. This would be similar to beginning development of other portions of an application by writing SQL statements or API specifications before fully deciding what the application should do. Instead, you should begin with a user experience. Then, work backwards from that experience to arrive at an implementation approach.
As you do this work, you’ll find yourself asking questions such as:
-
What are my resources? How are they organized? For example, do files reside within a folder?
-
Does the organization of the resources play a part in the permissions model?
-
What actions can principals perform on each resource?
-
How do principals acquire those permissions?
-
Do you want your end-users to choose from predefined permissions such as “Admin”, “Operator”, or “ReadOnly”, or should they create ad-hoc policy statements? Or both?
-
Are roles global or scoped? For example, is an "operator" limited within a single tenant, or does "operator" means operator across the whole application?
-
What types of queries are necessary to render the user experience? For example, do you need to list all of the resources that a principal can access to render that user's home page?
-
Can users accidentally lock themselves out of their own resources? Does that need to be avoided?
The end result of this exercise is referred to as an authorization model; it defines the principals, resources, actions, and how they interrelate to each other. Producing this model doesn’t require unique knowledge of Cedar or the Verified Permissions service. Instead, it is first and foremost a user experience design exercise, much like any other, and can manifest in artifacts such as interface mockups, logical diagrams, and an overall description of how permissions influence what users can do in the product. Cedar is designed to be flexible enough to meet customers at a model, rather than forcing the model to bend unnaturally to comply with a Cedar's implementation. As a result, gaining a crisp understanding of the desired user experience is the best way to arrive at an optimal model.
To help answer the questions and come to an optimal model, do the following:
Review Cedar design patterns
in the Cedar policy language Reference Guide. Consider the best practices
in the Cedar policy language Reference Guide. Consider the best practices included on this page.
Best practices
There isn't a canonical “correct” model
When you design an authorization model, there is no single, uniquely correct answer. Different applications can effectively use different authorization models for similar concepts, and this is OK. For example, consider the representation of a computer's file system. When you create a file in a Unix-like operating system, it doesn't automatically inherit permissions from the parent folder. In contrast, in many other operating systems and most online file-sharing services, files do inherit permissions from its parent folder. Both choices are valid depending upon the circumstances the application is optimizing for.
The correctness of an authorization solution isn’t absolute, but should be viewed in terms of how it delivers the experience that your customers want, and whether it protects their resources in the way they expect. If your authorization model delivers on this, then it is successful.
This is why beginning your design with the desired user experience is the most helpful prerequisite to the creation of an effective authorization model.
Focus on your resources beyond API operations
In most applications, permissions are modeled around the resources supported. For example, a file-sharing application might represent permissions as actions that can be performed on a file or a folder. This is a good, simple model that abstracts away the underlying implementation and the backend API operations.
In contrast, other types of applications, particularly web services, frequently design
permissions around the API operations themselves. For example, if a web service provides
an API named createThing()
, the authorization model might define a
corresponding permission, or an action
in Cedar named
createThing
. This works in many situations and makes it easy to
understand the permissions. To invoke the createThing
operation, you need
the createThing
action permission. Seems simple, right?
You'll find that the getting started process in the Verified Permissions console includes the option to build your resources and actions directly from an API. This is a useful baseline: a direct mapping between your policy store and the API that it authorizes for.
However, as you further develop your model, this API-focused approach may not be a good fit for applications with very granular authorization models because APIs are merely a proxy for what your customers are truly trying to protect: the underlying data and resources. If multiple APIs control access to the same resources, it can be difficult for administrators to reason about the paths to those resources and manage access accordingly.
For example, consider a user directory that contains the members of an organization. Users can be organized into groups, and one of the security goals is to prohibit discovery of group memberships by unauthorized parties. The service managing this user directory provides two API operations:
-
listMembersOfGroup
-
listGroupMembershipsForUser
Customers can use either of these operations to discover group membership. Therefore, the permissions administrator must remember to coordinate access to both operations. This is complicated further if you later choose to add a new API operation to address additional use cases, such as the following.
-
isUserInGroups
(a new API to quickly test if a user belongs in one or more groups)
From a security perspective, this API opens a third path for discovering group memberships, disrupting the carefully crafted permissions of the administrator.
We recommend that you focus on the underlying data and resources and their association
operations. Applying this approach to the group membership example would lead to an
abstract permission, such as viewGroupMembership
, which each of the three
API operations must consult.
API Name | Permissions |
---|---|
listMembersOfGroup |
requires viewGroupMembership permission on the
group |
listGroupMembershipsForUser |
requires viewGroupMembership permission on the
user |
isUserInGroups |
requires viewGroupMembership permission on the
user |
By defining this one permission, the administrator successfully controls access to discovering group memberships, now and forever. As a tradeoff, each API operation must now document the possibly several permissions that it requires, and the administrator must consult this documentation when crafting permissions. This can be a valid tradeoff when necessary to meet your security requirements.
Multi-tenancy considerations
You might want to develop applications for use by multiple customers - businesses that consume your application, or tenants - and integrate them with Amazon Verified Permissions. Before you develop your authorization model, develop a multi-tenant strategy. You can manage the policies of your customers in one shared policy store, or assign each a per-tenant policy store. For more information, see Amazon Verified Permissions multi-tenant design considerations in AWS Prescriptive Guidance.
-
One shared policy store
All tenants share a single policy store. The application sends all authorization requests to the shared policy store.
-
Per-tenant policy store
Each tenant has a dedicated policy store. The application will query different policy stores for an authorization decision, depending on the tenant that makes the request.
Neither strategy will have a large impact on your AWS bill. So how, then, should you design your approach? The following are common conditions that might contribute to your Verified Permissions multi-tenancy authorization strategy.
- Tenant policies isolation
-
Isolation of the policies of each tenant from the others is important to protect tenant data. When each tenant has their own policy store, they each have their own isolated set of policies.
- Authorization flow
-
You can identify a tenant making an authorization request with a policy store ID in the request, with per-tenant policy stores. With a shared policy store, all requests use the same policy store ID.
- Templates and schema management
-
When your application has multiple policy stores, your policy templates and a policy store schema add a level of design and maintenance overhead in each policy store.
- Global policies management
-
You might want to apply some global policies to every tenant. The level of overhead for management of global policies varies between shared and per-tenant policy store models.
- Tenant off-boarding
-
Some tenants will contribute elements to your schema and policies that are specific to their case. When a tenant is no longer active with your organization and you want to remove their data, the level of effort varies with their level of isolation from other tenants.
- Service resource quotas
-
Verified Permissions has resource and request-rate quotas that might influence your multi-tenancy decision. For more information about quotas, see Quotas for resources.
Comparing shared policy stores and per-tenant policy stores
Each consideration requires its own level of time and resource commitment in shared and per-tenant policy store models.
Consideration | Effort level in a shared policy store | Effort level in per-tenant policy stores |
---|---|---|
Tenant policies isolation | Medium. Must include tenant identifiers in policies and authorization requests. | Low. Isolation is default behavior. Tenant-specific policies are inaccessible to other tenants. |
Authorization flow | Low. All queries target one policy store. | Medium. Must maintain mappings between each tenant and their policy store ID. |
Templates and schema management | Low. Must make one schema work for all tenants. | High. Schemas and templates might be less complex individually, but changes require more coordination and complexity. |
Global policies management | Low. All policies are global and can be centrally updated. | High. You must add global policies to each policy store in onboarding. Replicate global policy updates between many policy stores. |
Tenant off-boarding | High. Must identify and delete only tenant-specific policies. | Low. Delete the policy store. |
Service resource quotas | High. Tenants share resource quotas that affect policy stores like schema size, policy size per resource, and identity sources per policy store. | Low. Each tenant has dedicated resource quotas. |
How to choose
Each multi-tenant application is different. Carefully compare the two approaches and their considerations before making an architectural decision.
If your application doesn't require tenant-specific policies and uses a single identity source, one shared policy store for all tenants is likely to be the most effective solution. This results in a simpler authorization flow and global policy management. Off-boarding a tenant using one shared policy store requires less effort because the application does not need to delete tenant-specific policies.
But if your application requires many tenant-specific policies, or uses multiple identity sources, per-tenant policy stores are likely to be most effective. You can control access to tenant policies with IAM policies that grant per-tenant permissions to each policy store. Off-boarding a tenant involves deleting their policy store; in a shared-policy-store environment, you must find and delete tenant-specific policies.