> ## Documentation Index
> Fetch the complete documentation index at: https://www.osohq.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# An End-to-End Example Securing a Real World App with Oso Cloud

In this tutorial, we'll cover the three main pieces involved in adding
authorization to a real world app with Oso Cloud.

* Modeling an authorization policy
* Declaring authorization as facts'
* Enforcing authorization decisions

This guide is for engineers after completing [the
quickstart](/get-started/quickstart) and want to explore how
Oso Cloud fits into a production application.

The hypothetical app we will explore in this guide is
[GitCloud](https://github.com/osohq/gitcloud), a GitHub/GitLab-like app
architected into two microservices. The first is a Python
service that manages accounts, organizations, and
repositories. The second is a Node.js service that manages GitCloud Jobs, a
Continuous Integration / Continuous Deliver (CI/CD) offering.

First, we use Oso Cloud's enforcement APIs to answer authorization queries.
For example, "is `User:alice` allowed to `"read"` the `Repository:foo` repository?"

Then, we model
an authorization policy for Oso Cloud using
[common Polar building blocks](/develop/policies/rbac).

Finally, we'll see how to maintain
authorization data in Oso Cloud via fact management.

## Initial setup

First we need to install Oso Cloud SDKs for GitCloud's two services

```sh theme={null}
# In the Python project
pip install oso-cloud

# In the Node.js project
yarn add oso-cloud
```

The clients are stateless, so we initialize a global singleton in each
service to avoid repeating the API key:

```python __init__.py theme={null}
# In the Python service
import os 

from oso_cloud import Oso
oso = Oso(api_key=os.environ.get("OSO_AUTH", None))
```

```js app.js theme={null}
// In the Node.js service
const { Oso } = require("oso-cloud");
const oso = new Oso("https://api.osohq.com/", process.env["OSO_AUTH"]);
```

In the Python service, we create a few helpers for
calling Oso Cloud's `authorize`, `list`, and `actions` enforcement APIs. This will avoid
some repetitiveness across the various controllers we have to update:

```python theme={null}
def authorize(action: str, resource: Any) -> bool:
    actor = current_user()
    resource = object_to_oso_value(resource)
    return oso.authorize(actor, action, resource)

def list_resources(action: str, resource_type: str) -> List[str]:
    if g.current_user is None:
        return []
    return oso.list(
        current_user(),
        action,
        resource_type
    )

def actions(resource: Any) -> List[str]:
    if g.current_user is None:
        return []
    actor = current_user()
    resource = object_to_oso_value(resource)
    return oso.actions(actor, resource)
```

We also define a few helpers for converting our domain objects into the
`oso_cloud.Value` objects that Oso Cloud accepts:

```python theme={null}
def current_user() -> oso_cloud.Value:
    if g.current_user is None:
        raise Unauthorized
    return object_to_oso_value(g.current_user)

def object_to_oso_value(obj: Any) -> oso_cloud.Value:
    if isinstance(obj, dict):
        return {"type": obj["type"], "id": str(obj["id"])} # make sure ids are stringified
    elif isinstance(obj, User):
        return {"type": "User", "id": str(obj.username)}
    else:
        return {"type": obj.__class__.__name__, "id": str(obj.id)}
```

With setup complete, you will add authorization checks to our app.

## Enforcement

Next, we enforce authorization across both services.

Enforcement happens in the logic reacting to the authorization decision in Oso.

For example in a HTTP application, `403 Forbidden` is returned when the
authorization decision is `False`. `200 OK` is returned when the authorization decision
is `True`.

We begin with enforcement to implement Oso Cloud incrementally and observe the
authorization results change as we progress.

<Info>
  **Testing your enforcement setup**
  When implementing authorization enforcement for the first time, it can be
  helpful to confirm that the integration is working as expected before
  introducing more policy logic.

  We'll do that by implementing a policy that allows everything, then one that
  denies everything, then observe how the app responds.

  Oso Cloud's default (empty) policy denies everything which should return `true`.

  With the "allow everything" policy, every request for an access-controlled
  resource should return `true`.

  With the "deny everything" (or empty) policy, every request should return
  `false`.

  If your app behaves this way, it means enforcement is working correctly and
  your authorization API calls are implemented properly

  **Important:** This is a testing-only strategy. It should never be used in production environments. Always remember to replace these placeholder policies with your real authorization logic.

  To push an "allow everything" policy to Oso Cloud via the CLI for testing:

  ```sh theme={null}
  echo "allow(_, _, _);" > authorization.polar
  oso-cloud policy authorization.polar
  ```

  To push a "deny everything" policy:

  ```sh theme={null}
  echo "" > authorization.polar
  oso-cloud policy authorization.polar
  ```

  Some authorization systems support both `allow(...) if ...;` and `deny(...) if ...;` semantics. In Oso Cloud, the idiomatic way to express this is by using an allow rule with negation. For example:

  ```polar theme={null}
  allow(actor: Actor, action: String, resource: Resource) if
    has_permission(actor, action, resource) and
    not deny(actor, action, resource);
  ```

  Here, deny is just a placeholder—like is\_banned(actor) or any other condition that should override normal access.
</Info>

### Authorizing an action on a specific resource

Most operations in GitCloud involve a user performing an
action on a specific resource. For example:

* Viewing a repository
* canceling a job
* updating an organization

For these scenarios,
we add enforcement by calling Oso Cloud's
[`authorize()`](/reference/sdks/authorization-checks)
API, which checks if an actor is allowed to perform an action on a specific resource.

For example, here's an updated handler for viewing a specific
organization:

```python focus=2:3 theme={null}
def show(org_id):
    if not authorize("read", {"type": "Organization", "id": org_id}):
        raise NotFound
    org = g.session.get_or_404(Organization, id=org_id)
    return org.as_json()
```

We ask Oso Cloud if the current user is allowed to `"read"` the organization.
We use the same pattern for most of the endpoints across both services.

<Info>
  Note that we chose to raise a `NotFound` error instead of `Forbidden`. This
  ensures that someone who doesn't have access to the repository cannot tell if
  it exists.
</Info>

### Authorizing an action on a collection of resources

The second most common operation in GitCloud is the "index" endpoint. It
displays a collection of resources, such as organizations that a user belongs
to or issues opened on a particular repository. The naïve implementation to
add enforcement on an index endpoint loops over the collection of
resources loaded from the database. Each iteration then uses Oso Cloud's `authorize()` API
to request an authorization decision.

Oso Cloud can wrap those authorization
checks into a single efficient list filtering query: the `list()` API.

While `authorize()`
poses the question "can `actor` perform `action` on `resource`?", `list()` asks
"for which resources of type `resource_type` can `actor` perform `action`?"

<Tip>
  For a deeper dive into the `list()` endpoint, head over to the [list filtering
  guide](/develop/enforce/list-filtering).
</Tip>

Oso Cloud's `list()` API behaves similar to `authorize()` except that it takes a
resource *type*, e.g., the `"Organization"` type instead of a specific
`{"type": "Organization", "id": 1}` instance. As an example of adding enforcement
for a collection of resources, here is the updated index handler for organizations:

```python focus=2,5 theme={null}
def index():
    authorized_ids = list_resources("read", "Organization")
    orgs = (
        g.session.query(Organization)
        .filter(Organization.id.in_(authorized_ids))
        .order_by(Organization.id)
    )
    return jsonify([o.as_json() for o in orgs])
```

Oso Cloud returns which organizations the current user can `"read"` as a
the collection of IDs. That collection of IDs can be used to load organization
objects from the service
database.

Fetching a collection of authorized IDs from Oso
Cloud's `list()` API and then loading those resources from the database is a
pattern that can be reused across various endpoints.

### Conditional UI elements

The final enforcement pattern covered in this tutorial is
conditionally displaying UI elements based on the current user's permissions.

For example, this pattern might gray out a button if the user isn't allowed to perform the
action represented.

Imagine this pattern in action with the GitCloud user interface (UI) that includes a page for
respository administrators to add and remove users. Users who aren't
administrators should not see any links to it inside the UI.

To fetch the set of actions that the current user is allowed to perform
on a repository efficiently , use Oso Cloud's
[`actions()`](/reference/sdks/authorization-checks)
API.

Here's the updated handler for viewing a specific repository:

```python focus=6 theme={null}
def show(org_id, repo_id):
    if not authorize("read", {"type": "Repository", "id": repo_id}):
        raise NotFound
    repo = g.session.get_or_404(Repository, id=repo_id, org_id=org_id)
    json = repo.as_json()
    json["permissions"] = actions(repo)
    return json
```

The call to `actions()` will return a list of actions that the
current user is allowed to perform on the `repo` repository. We include these
in the JSON response, so that our UI can check for the `"manage_members"`
permission. Based on the permission, the UI shows or hides the link
to the admin page as necessary.

## Modeling

Now that enforcement is implemented across our services, we will model our
policy and push it to Oso Cloud.

<Tip>
  Polar files can be kept in version control. It also support test for rigorous
  CI/CD to prevent regressions.
</Tip>

First, we establish the policy we need to express. We will start with
[Role-based Access Control](/develop/policies/rbac) - a common pattern found in
many authorization systems.

### Standard RBAC

Role-based Access Control establishes that a user can perform an
action on a resource if they have a role for that resource. We'll
start with a simple RBAC base policy.

```polar authorization.polar theme={null}
actor User {}

resource Repository {
  roles = ["reader"];
  permissions = ["read"];

  "read" if "reader";
}
```

Actors with the `"reader"` role on a specific
repository can `"read"` that repository. As we add additional roles and
permissions governing access to repositories, we'll add them to the resource
block. New RBAC rules are declared with the `<permission> if <role>;` form.
We employ the
exact same pattern to fill out the base policy for the other access-controlled
types in our app: organizations, issues, jobs, and users.

### Resource hierarchies / multitenancy

Hierarchy patterns often emerge in applications like our GitCloud example. One
such hierarchy would be an organization having many repositories and a repository
having many issues and jobs. In these cases, access flows from parent resources
to child resources. If a user has the
`"member"` role on the ACME organization, the user should have the same access to
one of ACME's repositories as a user with the `"reader"` role on that specific
repository.

The naïve implementation grants each `"member"`
an explicit `"reader"` role on each repository in the ACME org. Instead
of managing so many extra roles, we can define a relationship between the
`Organization` and `Repository` types. In it, we declare that an actor with
the `"member"` role on a
repository's parent organization can do anything a `"reader"` can
do on the repository.

The policy is updated with to reflect this hierarchy.

```polar focus=3:9,14,18 authorization.polar theme={null}
actor User {}

resource Organization {
  roles = ["member"];
  permissions = ["read", "create_repositories"];

  "create_repositories" if "member";
  "read" if "member";
}

resource Repository {
  roles = ["reader"];
  permissions = ["read"];
  relations = { organization: Organization };

  "read" if "reader";

  "reader" if "member" on "organization";
}
```

### Sharing

In our hypothetical GitCloud app, users should be able to invite others to
organizations and repositories. Managing the roles of users who have access
to a particular organization or repository is another requirement.

This is [pattern is common](/develop/policies/rbac#resource-ownership) and can
be implemented with resource ownership in Polar.

```polar focus=14,18,24 authorization.polar theme={null}
actor User {}

resource Organization {
  roles = ["member"];
  permissions = ["read", "create_repositories"];

  "create_repositories" if "member";
  "read" if "member";
}

resource Repository {
  roles = [
    "reader",
    "admin",
  ];
  permissions = [
    "read",
    "manage_members",
  ];
  relations = { organization: Organization };

  "read" if "reader";

  "manage_members" if "admin";

  "reader" if "member" on "organization";
}
```

### Ownership

The final pattern we implement for GitCloud is ownership. For this example, we
will implement authorization for closing issues. A user should be able to close
any issue they create. Looking
at [the Ownership in RBAC example](/develop/policies/rbac#resource-ownership),
the easiest way to represent ownership is
with a role on a resource.

```polar focus=29:37 authorization.polar theme={null}
actor User {}

resource Organization {
  roles = ["member"];
  permissions = ["read", "create_repositories"];

  "create_repositories" if "member";
  "read" if "member";
}

resource Repository {
  roles = [
    "reader",
    "admin",
  ];
  permissions = [
    "read",
    "manage_members",
  ];
  relations = { organization: Organization };

  "read" if "reader";

  "manage_members" if "admin";

  "reader" if "member" on "organization";
}

resource Issue {
  roles = ["creator"];
  permissions = ["read", "close"];
  relations = { repository: Repository };

  "close" if "creator";

  "read" if "reader" on "repository";
}
```

### Pushing the policy to Oso Cloud

Finally, we're ready to push our initial policy, saved as `authorization.polar`, with
the Oso Cloud CLI:

```sh theme={null}
oso-cloud policy authorization.polar
```

<Tip>
  There are many patterns that apply to GitCloud. If you're looking for a challenge
  or to improve your authorization understanding, try cloning [the GitCloud
  repo](https://github.com/osohq/gitcloud) and implementing a new pattern from
  [the Polar documentation](/develop/policies/overview).
</Tip>

## Data management

The final step of implementing authorization with Oso Cloud is to provide
authorization data as facts. In the modeling step,
we defined the abstract authorization policy for GitCloud. Now we need to
start provide role assignments (`User:alice` has the `"admin"` role
on `Repository:foo`) and relationships (`Organization:acme` is the `"organization"` of `Repository:foo`)
to Oso Cloud.

When we create a new resource in GitCloud, such as a new repository or
organization, we must supply Oso Cloud data about role assignments and relationships
pertaining to the new resource. For example, when a user creates a new repository,
Oso Cloud requires facts that the user has the `"admin"` role on that repository
*and* an `"organization"` relationship between the repository and its
parent organization exists.

In Oso Cloud, role assignments persist as `has_role`
facts of the form `has_role(actor, role, resource)`, e.g.,
`has_role(User:alice, "admin", Repository:foo)`.
Similarly, relationships are
represented as `has_relation` facts of the form `has_relation(related_resource, relation, resource)`, e.g., `has_relation(Repository:foo, "organization", Organization:acme)`.

To provide the pair of new facts to Oso Cloud when creating a new repository, we'll
use the Python client's
`batch()`.

```python focus=14:18 theme={null}
def create(org_id):
    org_value = {"type": "Organization", "id": str(org_id)}
    if not authorize("read", org_value):
        raise NotFound
    if not authorize("create_repositories", org_value):
        raise Forbidden("you do not have permission to create repositories")

    payload = request.get_json(force=True)

    repo = Repository(name=payload["name"], org_id=org_id)
    g.session.add(repo)
    g.session.commit()

    repo_value = {"type": "Repository", "id": str(repo.id)}
    with oso.batch() as txt:
        tx.insert({ "name": "has_relation",
                    "args": [repo_value, "organization", org_value] })
        tx.insert({ "name": "has_role",
                     "args": [current_user(), "admin", repo_value] })
    return repo.as_json(), 201
```

<Tip>
  If we only needed to create a single fact, we could use the
  [`tell()`](/reference/sdks/facts) API.
</Tip>

We also need to update or delete facts from Oso Cloud
in order to keep GitCloud's authorization data up-to-date.

For example, when a
user is removed from an organization, we need to delete any role assignments in
Oso Cloud. We could list and delete facts individually, but instead we'll use Oso
Cloud's
[`batch()`](/reference/sdks/facts#batch-operations)
API.

```python focus=13 theme={null}
def org_delete(org_id):
    payload = request.get_json(force=True)
    org_value = {"type": "Organization", "id": str(org_id)}
    permissions = actions(org_value)
    if not "read" in permissions:
        raise NotFound
    elif not "manage_members" in permissions:
        raise Forbidden

    org = g.session.get_or_404(Organization, id=org_id)

    user = {"type": "User", "id": payload["username"]}
    with oso.batch() as tx:
        tx.delete([{"name": "has_role", "args": [user, None, org_value]}])

    return {}, 204
```

In the `batch()` API, the `None` argument acts as a wildcard, instructing Oso
Cloud to delete any roles the user may have on the given organization.

Using the
`batch()` API is useful when there are multiple roles for a user. If we needed
to delete a single fact, we use the
[`delete()`](/reference/sdks/facts#delete-facts)
API instead.

Additionally, if wildcards are undeed, you can delete
many facts at once with pairing [`batch()` with `delete()`](/reference/sdks/facts#batch-operations).

### Using context facts

Sometimes it is helpful to be able to provide more authorization data to Oso
Cloud when requesting an authorization decision.
For example, the python GitCloud service is the only service that knows about
issues which are far more numerous than repositories or organizations.

Do we have to store issue-related facts in Oso Cloud?

In situations like these, we can take advantage of [context
facts](/develop/facts/context-facts). Context facts are considered only
for the duration of a single request. Rather than using the `tell()` and
`delete()` APIs to sync data about issues to Oso Cloud, we can pass context
facts into any of the authorization APIs, including `authorize()` and `list()`.

Update our `authorize()` helper function to fetch authorization-relevant data about issues from
our database and pass it along as context facts:

```python focus=4:7,10:33 theme={null}
def authorize(action: str, resource: Any) -> bool:
    actor = current_user()
    resource = object_to_oso_value(resource)
    context_facts = []
    if resource["type"] == "Issue":
        context_facts = get_facts_for_issue(resource["id"])
    res = oso.authorize(actor, action, resource, context_facts)
    return res

def get_facts_for_issue(issue_id):
    issue = g.session.get_or_404(Issue, id=issue_id)

    issue_value = {"type": "Issue", "id": str(issue.id)}

    has_parent = {
        "name": "has_relation",
        "args": [
          issue_value,
          "repository",
          {"type": "Repository", "id": str(issue.repo_id)}
        ],
    }
    creator = {
        "name": "has_role",
        "args": [
            {"type": "User", "id": str(issue.creator_id)},
            "creator",
            issue_value,
        ],
    }
    facts = [has_parent, creator]

    return facts
```

Now we can call `authorize("close", issue)` in a handler to ensure that the current user
is authorized to `"close"` a given issue. Our helper will pass along two critical `has_role`
and `has_relation` facts so that Oso Cloud can make the correct authorization decision.

Context facts are powerful, but they're better suited for certain situations
than others. Be sure to [read more about context
facts](/develop/facts/context-facts) before using them in your
application.

## Summary

In this tutorial, we covered the three main pieces involved in adding
authorization to a real world app with Oso Cloud:

* Modeling a policy
* Managing authorization data as facts
* Enforcing authorization decisiosn

We used Oso Cloud's `authorize()` and `list()` APIs to answer
authorization questions posed about specific resources and collections of
resources.

We learned how to lean on [Polar](/develop/policies/overview) to model common authorization
patterns in Oso Cloud. Finally, we learned how to keep authorization data
synchronized in Oso Cloud via fact management APIs.

## Next Steps

For next steps, if you're interested in securing your *own* app with Oso Cloud,
head over to the [Enforcement
guide](/develop/enforce/authorize-requests).

If you want to play
around with GitCloud, clone [the repo](https://github.com/osohq/gitcloud) and
try implementing some new authorization patterns from the Modeling Patterns
reference.
