{"id":18576181,"url":"https://github.com/jreyesr/mongo-oso","last_synced_at":"2026-02-24T02:05:29.306Z","repository":{"id":131446889,"uuid":"509294475","full_name":"jreyesr/mongo-oso","owner":"jreyesr","description":"An Adapter for the Oso authorization library and MongoDB","archived":false,"fork":false,"pushed_at":"2025-01-28T02:14:10.000Z","size":20,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-01-28T03:23:04.413Z","etag":null,"topics":["abac","access-control","authorization","authorization-framework","oso-policy-language","rbac"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jreyesr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-01T02:22:56.000Z","updated_at":"2025-01-28T02:14:14.000Z","dependencies_parsed_at":"2023-05-12T11:15:22.128Z","dependency_job_id":null,"html_url":"https://github.com/jreyesr/mongo-oso","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jreyesr%2Fmongo-oso","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jreyesr%2Fmongo-oso/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jreyesr%2Fmongo-oso/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jreyesr%2Fmongo-oso/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jreyesr","download_url":"https://codeload.github.com/jreyesr/mongo-oso/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239311611,"owners_count":19618013,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["abac","access-control","authorization","authorization-framework","oso-policy-language","rbac"],"created_at":"2024-11-06T23:23:53.962Z","updated_at":"2025-11-01T18:30:22.204Z","avatar_url":"https://github.com/jreyesr.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# oso-mongo-adapter\n\nA [Data Adapter](https://docs.osohq.com/reference/frameworks/data_filtering.html) that integrates the [Oso authorization library](https://www.osohq.com/) with MongoDB databases.\n\n## What works?\n\nData filtering :)\n\nData filtering allows you to call `oso.authorized_resources(user, \"read\", ResourceClass)` and get back a list of all objects of type `ResourceClass` that the `user` can `read`. Obtaining an authorized query (i.e., the raw DB query that would generate the list) is also supported, in case you want to apply further filtering, sorting or pagination, for example.\n\nSee below (the [Limitations section](#limitations)) for what _doesn't_ work\n\n## How to use?\n\nSee `main.py` for a working example.\n\n1. Write your policy as normal.\n2. Implement [Pydantic models](https://pydantic-docs.helpmanual.io/) for each collection and nested document. The classes that correspond to collections must have a `__coll_name__ = \"collection_name\"` attribute-.\n3. Instantiate a [MongoClient from the pymongo library](https://pymongo.readthedocs.io/en/stable/tutorial.html).\n4. Register a `MongoAdapter` instance with Oso,, passing it a database connection.\n5. Register the Pydantic classes with `oso.register_class`. You only need to declare the fields that are used for policy decisions.\n6. \n    For nested documents, when registering the outer class, declare the inner field as a `Relation` with `kind=one`, `my_field` set to the key of the nested document, and `other_field` set to an empty string (this is the marker that the library uses to determine that the relation is a nested field and not a lookup). For example, consider the following MongoDB document (data model taken from [here](https://www.mongodb.com/docs/manual/tutorial/model-embedded-one-to-many-relationships-between-documents/#std-label-one-to-many-embedded-document-pattern)):\n    ```js\n    {\n        _id: \"joe\",\n        name: \"Joe Bookreader\",\n        address: {\n            street: \"123 Fake Street\",\n            city: \"Faketon\",\n            state: \"MA\",\n            zip: \"12345\"\n        }\n    }\n    ```\n    This resource would be registered with Oso as\n    ```py\n    # Register the inner/nested class\n    oso.register_class(Address, fields={\"street\": str, \"city\": str, \"state\": str, \"zip\": str})\n\n    # Register the outer class\n    oso.register_class(Patron, fields={\n        \"_id\": str, \"name\": str, \n        \"address\": Relation(kind=\"one\", other_type=\"Address\", my_field=\"address\", other_field=\"\")\n    })\n    ```\n7. \n    For one-to-many relationships that use document references, use the same pattern as when registering a SQL model. The `Relation` should have `other_field` set to `_id`. For example, for the following model (adapted from [here](https://www.mongodb.com/docs/manual/tutorial/model-referenced-one-to-many-relationships-between-documents/)):\n    ```js\n    // publishers collection\n    {\n        _id: \"oreilly\",\n        name: \"O'Reilly Media\",\n        founded: 1980,\n        location: \"CA\"\n    }\n\n    // books collection\n    {\n        _id: 123456789,\n        title: \"MongoDB: The Definitive Guide\",\n        author: [ \"Kristina Chodorow\", \"Mike Dirolf\" ],\n        published_date: ISODate(\"2010-09-24\"),\n        pages: 216,\n        language: \"English\",\n        publisher_id: \"oreilly\"\n    }\n    ```\n    The `publisher` field in the `books` collection is a link to the `_id` in the `publishers` collection. This would be expressed in your code as:\n    ```py\n    # Declare the Pydantic models\n    class Publisher(BaseModel):\n        _id: str\n        name: str\n        founded: int\n        location: str\n\n    class Book(BaseModel):\n        _id: int\n        title: str\n        author: List[str]\n        published_date: datetime.datetime\n        pages: int\n        language: str\n        publisher_id: str\n    \n    # Register the related-to class\n    oso.register_class(Publisher, fields={\"_id\": str, \"name\": str, \"founded\": int, \"location\": str})\n\n    # Register the class with the relationship\n    # NOTE datetime.datetime cannot be (easily?) used, so skip the published_date field\n    oso.register_class(Book, fields={\n        \"_id\": int, \"title\": str, \"author\": list, \"pages\": int, \"language\": str,\n        \"publisher\": Relation(kind=\"one\", other_type=\"Publisher\", my_field=\"publisher_id\", other_field=\"_id\")\n    })\n    ```\n8. Call `oso.load_files([\"policy.polar\"])`\n9. Whenever you need to authorize data access, call `oso.authorized_query(user, \"permission\", Model)`, add any further clauses, if required, and then call `.aggregate()` on the computed pipeline and return the results.\n\n### Logging\nThe `oso.adapter.mongo` logger prints all authorization queries at DEBUG level. Enable them if you wish to see the queries that are computed by the Oso engine.\n\n## Limitations\n\n- [Request-level enforcement](https://docs.osohq.com/guides/enforcement/request.html) is completely untested. I have hope it would work with no changes, since it should not touch the Adapter code at all, but who knows...\n- Only works with pymongo and Pydantic (sorry, everyone else!)\n- [Resource-level enforcement](https://docs.osohq.com/guides/enforcement/resource.html) (i.e., calling `oso.authorize(user, \"read\", some_object)`) doesn't work. To emulate it, apply [the data filtering API](https://docs.osohq.com/guides/data_filtering.html) to obtain the required authorization queries, and then append a new query to filter by `_id` or however else you would have obtained `some_object` otherwise:\n    ```py\n    # This ID probably comes from a URL segment, if developing a web application\n    org_id = 12345\n\n    q = oso.authorized_query(user, \"read\", Org)\n    pipeline = q[\"pipeline\"] + [\n        {\"$match\": {\"_id\": org_id}},\n    ]\n    try:\n        # Run the query and return the first element, if t exists\n        return next(q[\"model\"].aggregate(pipeline))\n    except StopIteration:\n        # Treat missing data just as unauthorized access, to leak no information to an attacker\n        # Do whatever your web framework does to raise an HTTP status code 404\n        raise Exception(\"404\")\n    ```\n- Relations across collections (implemented with ObjectID keys) are only tested in the simplest case (a single ID as a foreign key replacement in the root level of the document). The most exotic relationship patterns (as documented [here](https://www.mongodb.com/docs/manual/tutorial/model-embedded-one-to-one-relationships-between-documents/)) may or may not work.\n- Extremely untested! (Basically, it only implements the functionality required for my own use cases, and no more)\n\n## Internals\n\nThe adapter uses MongoDB aggregation pipelines to express the conditions.\n\n### Gotcha: `oso.authorized_query` is somewhat different to the official relational adapters\n\nFor the SQL (relational) adapters, `oso.authorized_query(me, \"read\", Org)` would return a pre-authorized query to which you could append more conditions if required:\n```py\n# NOTE This is code for the SQLAlchemy adapter, it doesn't work for Mongo\nq = oso.authorized_query(user, \"read\", Org)\nq = q.skip(0).limit(10)\nreturn q.all()\n```\nHowever, this relies on the SQLAlchemy and Django ORMs allowing chaining of operations (for example, on Django, `Org.objects.filter(name=\"\").filter(num_members__lt=50)[:10]` is a valid query, and it employs some sort of [fluent interface](https://www.martinfowler.com/bliki/FluentInterface.html) pattern. SQLAlchemy does more or less the same). However, on Mongo, doing `db[\"collname\"].aggregate(...)` does NOT return an object on which further `.find()`s, `.skip()`s or `.aggregate()`s can be called, but a cursor that is already (more or less) a finalized query. Therefore, the `authorized_query()` method returns a different set of data:\n```py\n\u003e\u003e\u003e oso.authorized_query(user, \"read\", Org)\n{'model': Collection(Database(...), 'mydb'), 'orgs'), 'pipeline': [{'$match': {'$or': [...]}}]}\n```\nThe method returns a dictionary with two keys: `model`, that returns the base/root model for the query (the result of `db[\"mycoll\"]`); and `pipeline`, that returns a dictionary that can be plugged into a `.aggregate()` method call or extended with further stages beforehand. An example of usage with search and skip-limit pagination would be:\n```\n# These could come from the query string, if developing a web application\nsearch_term = \"Inc.\"\nskip = 0\nlimit = 10\n\n# SQLAlchemy adapter\nilike = \"%{}%\".format(search_term)\nq = oso.authorized_query(user, \"read\", Org).filter(Org.name.ilike(ilike)).limit(limit).offset(skip)\nreturn q.all()\n\n# Django adapter\nq = oso.authorized_query(user, \"read\", Org).filter(name__icontains=search_term)\nreturn q[skip:limit]\n\n# MongoAdapter\nq = oso.authorized_query(user, \"read\", Org)\n# Extend the pipeline with some more filter and pagination stages\npipeline = q[\"pipeline\"] + [\n    {\"$match\": {\"name\": re.compile(search_term, re.IGNORECASE)}},\n    { \"$skip\": skip },\n    { \"$limit\": limit },\n]\nreturn list(q[\"model\"].aggregate(pipeline))\n```\n\n### Filtering\n\nA single [`$match` stage](https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/) is used to apply the filters. Inside the stage, the conditions are expressed as a disjunction of conjunctions, AKA `(x1 AND x2) OR (y1) OR (z1 AND z2 AND z3) OR ...`, AKA [conjunctive normal form](https://en.wikipedia.org/wiki/Conjunctive_normal_form). \n\nSome optimizations are used to remove useless operators: for example, in `OR(AND(x1, x2), AND(y1), AND(z1, z2, z3), ...)`, the `AND(y1)` part can be replaced by `y1`.\n\n### Lookups (cross-collection data)\n\nIn case your policies require jumping across collections, [`$lookup` stages](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/) and [`$unwind` stages](https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/) are added as required. These serve the function of a `JOIN` statement in a SQL database, in that they take an ID field in a document and replace it with a full document that comes from another collection.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjreyesr%2Fmongo-oso","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjreyesr%2Fmongo-oso","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjreyesr%2Fmongo-oso/lists"}