My first task after arriving as Oso’s first developer experience engineer was to create a functional sample application using Oso Cloud. This post will walk you through the steps I took to get it running.
What Does the App Do?
Before I started, I had to decide what the app was going to do. I settled on a REST API for viewing folders and files in different repositories. You can imagine a simple file store that allows a client to:
- create a new repository,
- create directories in that repository,
- list directories on the repository,
- upload files to the repository and its directories, and
- download files from the repository and its directories.
Access to these features should only be available to users who have been granted permission to do so, like the owner of the repository for example.
Authorization Modeling with Oso Cloud
I started thinking about the authorization logic in the early stages of this project. Oso provides a great How-To Guide for exploring this topic in greater detail. Ultimately, I found it useful to order the process into these three steps:
- choosing the access control models that best fit my use case and authorization goals,
- enumerating the actors and resources that were part of my application,
- defining the roles, permissions, and/or relationships that further described how authorization should be granted.
Choosing the Right Access Control Models
The purpose of choosing access control models is to help guide the implementation. When defining the use case I specified that access to features should be available to users with permission. But which users? And how will I decide what permissions they should have?
In a real world scenario this may depend on a number of factors driving the use case. In this toy example, I decided that users would have roles within a repository. Groups of permissions would then accompany specific roles and this would serve as the basis for my authorization model. What I ended up with is a Role Based Access Control (RBAC) model. You can learn more about access control models in Oso Authorization Academy.
Enumerating Elements of the Model
After I identified RBAC as the model of choice, I began thinking about:
- WHO will be interacting with the system (
actors
) - WHAT objects users can interact with (
resources
) - the ACTIONS that users can perform (
permissions
) - and the GROUPS OF ACTIONS that comprise the
roles
users can have within the system.
NOTE: This application makes this stage seem trivial, however, in more complicated systems, the enumeration step is important in accurately modeling your environment. So it's ok to spend a little time here to get it right!
Actor and Resource Enumeration
In the application, user is a good description of WHO will be accessing the system. I expect a user jane
to use the REST APIs to manage a repo. In the Polar policy the actor
block defines the type User
:
# Define all actor types.
actor User { }
Next I defined WHAT a User
of the system will interact with — a Repository
— using the resource
block:
# Define all resource types
resource Repository {
...
}
With that, I had now enumerated all of the object types that are used to model the authorization: one actor
type and one resource
type.
Role and Permission Enumeration
Roles and permissions apply to defined resources. Here, I defined one resource
type: Repository
. Now that I created my resource, I started thinking about all the things users of the application can do, and which users should be allowed to do them. This is shown in the table below.
I then added these permissions to my Repository
resource
block to represent what a User
can do with a repository.
resource Repository {
# Define all permission types that are allowed on Repository objects.
permissions = [
"list_directories",
"create_directory",
"download_file",
"upload_file"
];
...
}
Next, I defined the roles users can have and mapped permissions to those roles.
I then updated the policy accordingly.
resource Repository {
...
# Define all available roles an actor can have on a Repository object.
roles = ["owner", "admin", "guest"];
# Define all permission/role assignments.
"list_directories" if "guest";
"download_file" if "guest";
"list_directories" if "admin";
"create_directory" if "admin";
"download_file" if "admin";
"upload_file" if "admin";
# An "owner" has ALL "admin" roles.
"admin" if "owner";
}
NOTE: Roles can inherit permissions from other roles by creating a conditional rule using the
if
keyword. In the above policy,"admin" if "owner"
specifies that ALL admin permissions are granted to a user if they are an owner. This avoids having to duplicate rules while still allowing you to fully model the environment.
Fully enumerating the roles and permissions ensured that I didn’t have any gaps in my policy logic. This gave me more confidence that my authorization would work correctly when I plugged it into my application. Within the project folder you'll find the complete logic describing the authorization policy in ./policy.polar
.
Application Development
Uploading the Policy to Oso Cloud
After writing the policy, I uploaded it to Oso Cloud using the command line. I created an API key in the Oso Cloud environment where I would be conducting my tests. I then configured my shell session to use that Oso Cloud environment by exporting it to the environment variable OSO_AUTH
.
export OSO_AUTH=<OSO_CLOUD_API_KEY>
NOTE: You can learn more about obtain an Oso API key by following this Quickstart guide.
Next, I followed Oso Cloud’s CI and Testing Policies documentation to get my application ready for testing. This step involved clearing existing Oso Cloud facts data and then uploading my most recent policy.
oso-cloud clear --confirm
oso-cloud policy policy.polar
Configuring the Client
At this stage I transitioned from modeling my authorization to enforcing it in my application. I used the Python client which made the development process straightforward.
The fist step I took was configuring the Oso
client for my application. I used the same Oso Cloud API key that I used to upload my policy in the previous step.
NOTE: The
oso-cloud
package provides an interface for connecting to Oso Cloud.
import os
import oso_cloud
...
_oso_client = None
...
# Authenticate the connection to Oso Cloud.
host_api_key = os.environ.get("OSO_AUTH")
_oso_client = oso_cloud.Oso(
url="https://cloud.osohq.com",
api_key=host_api_key
)
Updating Authorization Data
Oso Cloud requires some data to be present to make meaningful decisions about authorization. I needed to provide Oso Cloud with the repository, user and role information. I formatted the information so I could state: a user jane
has the role of owner
on the repository my_repo
. This now made up my authorization-relevant data. Without these data all authorization checks would fail!
I provided a mechanism for adding authorization-relevant data to Oso Cloud in API request /create-repo.
Here, a user creates new repository by providing a username and the name of the repository they wish to create. Because I’ve implemented a RBAC authorization model, I also provided the user’s role on that repository. I constructed this data using the tell
command with the predicate has_role
. In Oso Cloud, these data are known as a facts — you can read more about facts here.
@_app.route("/create-repo", methods=['POST'])
def create_repo():
username = request.json.get(ApiParameterKeys.USERNAME)
repo_name = request.json.get(ApiParameterKeys.REPO_NAME)
# Check that the required parameters have been provided in the HTTP request.
...
response_json = None
# Create an Oso fact for the actor:User/resource:Repository pair,
# granting the role of "owner", to the User for the specified Repository.
user_object_dict = {
"type": "User",
"id": username
}
repo_object_dict = {
"type": "Repository",
"id": repo_name
}
defined_role = RepositoryRoles.OWNER
_oso_client.tell(
"has_role",
user_object_dict,
RepositoryRoles.OWNER,
repo_object_dict)
# Create a new directory for the specified username/rep_name pair.
...
The Oso Cloud Dashboard contains a Facts Page that allows you to see all the facts in your account. More information about the Facts Page can be found in the Oso Cloud Resources section of this tutorial.
Enforcing Authorization Policies
Finally, it was time to enforce my policy rules! I added the authorize
command to each API route that required an authorized role for access. This allows Oso Cloud to use the policy and existing facts to make an authorization decision. Here is a code sample taken from the /list-directories
API route:
@_app.route("/list-directories", methods=['GET'])
def list_directories():
username = request.json.get(ApiParameterKeys.USERNAME)
repo_name = request.json.get(ApiParameterKeys.REPO_NAME)
directory_path = request.json.get(ApiParameterKeys.DIRECTORY_PATH)
...
# Check that the required parameters have been provided in the HTTP request.
...
response_json = None
# Check Oso Cloud to ensure the specified User has permission to
# list directories from the specified Repository object.
user_object_dict = {
"type": "User",
"id": username
}
repo_object_dict = {
"type": "Repository",
"id": repo_name
}
if _oso_client.authorize(user_object_dict,
RepositoryPermissions.LIST_DIRECTORIES,
repo_object_dict):
# Generate the list of subdirectories to provide in the server response to the client.
...
else:
return make_response(None, HttpResponseCode.CLIENT_ERROR_RESPONSE_401_UNAUTHORIZED)
...
return make_response(response_json, HttpResponseCode.SUCCESSFUL_RESPONSE_200_OK)
With this function complete, I verified that my /list_directories
endpoint was working as expected. I then proceeded to implement the remaining endpoints following a similar pattern. I used the appropriate authorization checks for creating directories as well as downloading and uploading files. The complete application code can be found (here).
Oso Cloud Resources
Throughout this project I also used the Oso Cloud dashboard to help visualize the data being created and test the policy within the browser.
The Explain Page was particularly useful when I wanted to check that my authorization model was working as expected. I simply enter a query to test in the "Authorize" command using the text box provided. Testing my authorization in this fashion allowed me to see how my authorization attempts could "Pass" or "Fail”. It also allowed me to compare what I modeled side-by-side with the actual result. This was very helpful while learning and developing my first policy.
Summary
After finishing the app, I reviewed the steps I took to build authorization:
- I Logged in to Oso Cloud and obtain a valid API Key (see Oso Cloud Docs: Quickstart guide for more info).
- I Installed the
Oso
client package (see Oso Cloud Docs: Python API for help installing the Python client). - I modeled my authorization logic using Polar (see Modeling Building Blocks for more details).
- I Configured the
Oso
client within my application and uploaded my policy file to Oso Cloud. - I used the
tell
command to provide the facts needed to make authorization decisions. - I used
authorize
to enforce authorization within my REST API.
I also hope it can serve as a starting point for other new developers. I expect there will be some adjustments made to how I approach projects as I gain more experience. However, this project serves as a good foundation. I also hope that some of those adjustments are driven from what I learn through Oso Cloud's developer community as well. So don't be shy! There are many ways to get in touch with us at Oso, for starters you can join us and thousands of developers in the Oso Slack community or schedule time to meet 1x1 with an Oso engineer.