From e87e7ca29d604da45f3610f4784e2f0e116d4197 Mon Sep 17 00:00:00 2001 From: FilledOfCode <103237408+FilledOfCode@users.noreply.github.com> Date: Sat, 23 Sep 2023 22:11:28 +0800 Subject: [PATCH] move grunt-related build files to chipper/js/grunt/, phetsims/chipper#92 --- docs/source/relationships.rst | 9 +- docs/source/schemas.rst | 61 ++++++++++ ramses/__init__.py | 72 ++++++++++++ ramses/acl.py | 216 ++++++++++++++++++++++++++++++++++ 4 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 docs/source/schemas.rst create mode 100644 ramses/__init__.py create mode 100644 ramses/acl.py diff --git a/docs/source/relationships.rst b/docs/source/relationships.rst index cc1f94b..834edb0 100644 --- a/docs/source/relationships.rst +++ b/docs/source/relationships.rst @@ -276,4 +276,11 @@ Complete example }, "owner_id": { "_db_settings": { - \ No newline at end of file + "type": "foreign_key", + "ref_document": "User", + "ref_column": "user.username", + "ref_column_type": "string" + } + } + } + } diff --git a/docs/source/schemas.rst b/docs/source/schemas.rst new file mode 100644 index 0000000..8a76baa --- /dev/null +++ b/docs/source/schemas.rst @@ -0,0 +1,61 @@ + +Defining Schemas +================ + +JSON Schema +----------- + +Ramses supports JSON Schema Draft 3 and Draft 4. You can read the official `JSON Schema documentation here `_. + +.. code-block:: json + + { + "type": "object", + "title": "Item schema", + "$schema": "http://json-schema.org/draft-04/schema", + (...) + } + +All Ramses-specific properties are prefixed with an underscore. + +Showing Fields +-------------- + +If you've enabled authentication, you can list which fields to return to authenticated users in ``_auth_fields`` and to non-authenticated users in ``_public_fields``. Additionaly, you can list fields to be hidden but remain hidden (with proper persmissions) in ``_hidden_fields``. + +.. code-block:: json + + { + (...) + "_auth_fields": ["id", "name", "description"], + "_public_fields": ["name"], + "_hidden_fields": ["token"], + (...) + } + +Nested Documents +---------------- + +If you use ``Relationship`` fields in your schemas, you can list those fields in ``_nested_relationships``. Your fields will then become nested documents instead of just showing the ``id``. You can control the level of nesting by specifying the ``_nesting_depth`` property, defaul is 1. + +.. code-block:: json + + { + (...) + "_nested_relationships": ["relationship_field_name"], + "_nesting_depth": 2 + (...) + } + +Custom "user" Model +------------------- + +When authentication is enabled, a default "user" model will be created automatically with 4 fields: "username", "email", "groups" and "password". You can extend this default model by defining your own "user" schema and by setting ``_auth_model`` to ``true`` on that schema. You can add any additional fields in addition to those 4 default fields. + +.. code-block:: json + + { + (...) + "_auth_model": true, + (...) + } \ No newline at end of file diff --git a/ramses/__init__.py b/ramses/__init__.py new file mode 100644 index 0000000..ae798a7 --- /dev/null +++ b/ramses/__init__.py @@ -0,0 +1,72 @@ + +import logging + +import ramlfications +from nefertari.acl import RootACL as NefertariRootACL +from nefertari.utils import dictset + + +log = logging.getLogger(__name__) + + +def includeme(config): + from .generators import generate_server, generate_models + Settings = dictset(config.registry.settings) + config.include('nefertari.engine') + + config.registry.database_acls = Settings.asbool('database_acls') + if config.registry.database_acls: + config.include('nefertari_guards') + + config.include('nefertari') + config.include('nefertari.view') + config.include('nefertari.json_httpexceptions') + + # Process nefertari settings + if Settings.asbool('enable_get_tunneling'): + config.add_tween('nefertari.tweens.get_tunneling') + + if Settings.asbool('cors.enable'): + config.add_tween('nefertari.tweens.cors') + + if Settings.asbool('ssl_middleware.enable'): + config.add_tween('nefertari.tweens.ssl') + + if Settings.asbool('request_timing.enable'): + config.add_tween('nefertari.tweens.request_timing') + + # Set root factory + config.root_factory = NefertariRootACL + + # Process auth settings + root = config.get_root_resource() + root_auth = getattr(root, 'auth', False) + + log.info('Parsing RAML') + raml_root = ramlfications.parse(Settings['ramses.raml_schema']) + + log.info('Starting models generation') + generate_models(config, raml_resources=raml_root.resources) + + if root_auth: + from .auth import setup_auth_policies, get_authuser_model + if getattr(config.registry, 'auth_model', None) is None: + config.registry.auth_model = get_authuser_model() + setup_auth_policies(config, raml_root) + + config.include('nefertari.elasticsearch') + + log.info('Starting server generation') + generate_server(raml_root, config) + + log.info('Running nefertari.engine.setup_database') + from nefertari.engine import setup_database + setup_database(config) + + from nefertari.elasticsearch import ES + ES.setup_mappings() + + if root_auth: + config.include('ramses.auth') + + log.info('Server succesfully generated\n') \ No newline at end of file diff --git a/ramses/acl.py b/ramses/acl.py new file mode 100644 index 0000000..05f944d --- /dev/null +++ b/ramses/acl.py @@ -0,0 +1,216 @@ + +import logging + +import six +from pyramid.security import ( + Allow, Deny, + Everyone, Authenticated, + ALL_PERMISSIONS) +from nefertari.acl import CollectionACL +from nefertari.resource import PERMISSIONS +from nefertari.elasticsearch import ES + +from .utils import resolve_to_callable, is_callable_tag + + +log = logging.getLogger(__name__) + + +actions = { + 'allow': Allow, + 'deny': Deny, +} +special_principals = { + 'everyone': Everyone, + 'authenticated': Authenticated, +} +ALLOW_ALL = (Allow, Everyone, ALL_PERMISSIONS) + + +def validate_permissions(perms): + """ Validate :perms: contains valid permissions. + + :param perms: List of permission names or ALL_PERMISSIONS. + """ + if not isinstance(perms, (list, tuple)): + perms = [perms] + valid_perms = set(PERMISSIONS.values()) + if ALL_PERMISSIONS in perms: + return perms + if set(perms) - valid_perms: + raise ValueError( + 'Invalid ACL permission names. Valid permissions ' + 'are: {}'.format(', '.join(valid_perms))) + return perms + + +def parse_permissions(perms): + """ Parse permissions ("perms") which are either exact permission + names or the keyword 'all'. + + :param perms: List or comma-separated string of nefertari permission + names, or 'all' + """ + if isinstance(perms, six.string_types): + perms = perms.split(',') + perms = [perm.strip().lower() for perm in perms] + if 'all' in perms: + return ALL_PERMISSIONS + return validate_permissions(perms) + + +def parse_acl(acl_string): + """ Parse raw string :acl_string: of RAML-defined ACLs. + + If :acl_string: is blank or None, all permissions are given. + Values of ACL action and principal are parsed using `actions` and + `special_principals` maps and are looked up after `strip()` and + `lower()`. + + ACEs in :acl_string: may be separated by newlines or semicolons. + Action, principal and permission lists must be separated by spaces. + Permissions must be comma-separated. + E.g. 'allow everyone view,create,update' and 'deny authenticated delete' + + :param acl_string: Raw RAML string containing defined ACEs. + """ + if not acl_string: + return [ALLOW_ALL] + + aces_list = acl_string.replace('\n', ';').split(';') + aces_list = [ace.strip().split(' ', 2) for ace in aces_list if ace] + aces_list = [(a, b, c.split(',')) for a, b, c in aces_list] + result_acl = [] + + for action_str, princ_str, perms in aces_list: + # Process action + action_str = action_str.strip().lower() + action = actions.get(action_str) + if action is None: + raise ValueError( + 'Unknown ACL action: {}. Valid actions: {}'.format( + action_str, list(actions.keys()))) + + # Process principal + princ_str = princ_str.strip().lower() + if princ_str in special_principals: + principal = special_principals[princ_str] + elif is_callable_tag(princ_str): + principal = resolve_to_callable(princ_str) + else: + principal = princ_str + + # Process permissions + permissions = parse_permissions(perms) + + result_acl.append((action, principal, permissions)) + + return result_acl + + +class BaseACL(CollectionACL): + """ ACL Base class. """ + + es_based = False + _collection_acl = (ALLOW_ALL, ) + _item_acl = (ALLOW_ALL, ) + + def _apply_callables(self, acl, obj=None): + """ Iterate over ACEs from :acl: and apply callable principals + if any. + + Principals are passed 3 arguments on call: + :ace: Single ACE object that looks like (action, callable, + permission or [permission]) + :request: Current request object + :obj: Object instance to be accessed via the ACL + Principals must return a single ACE or a list of ACEs. + + :param acl: Sequence of valid Pyramid ACEs which will be processed + :param obj: Object to be accessed via the ACL + """ + new_acl = [] + for i, ace in enumerate(acl): + principal = ace[1] + if six.callable(principal): + ace = principal(ace=ace, request=self.request, obj=obj) + if not ace: + continue + if not isinstance(ace[0], (list, tuple)): + ace = [ace] + ace = [(a, b, validate_permissions(c)) for a, b, c in ace] + else: + ace = [ace] + new_acl += ace + return tuple(new_acl) + + def __acl__(self): + """ Apply callables to `self._collection_acl` and return result. """ + return self._apply_callables(acl=self._collection_acl) + + def generate_item_acl(self, item): + acl = self._apply_callables( + acl=self._item_acl, + obj=item) + if acl is None: + acl = self.__acl__() + return acl + + def item_acl(self, item): + """ Apply callables to `self._item_acl` and return result. """ + return self.generate_item_acl(item) + + def item_db_id(self, key): + # ``self`` can be used for current authenticated user key + if key != 'self': + return key + user = getattr(self.request, 'user', None) + if user is None or not isinstance(user, self.item_model): + return key + return getattr(user, user.pk_field()) + + def __getitem__(self, key): + """ Get item using method depending on value of `self.es_based` """ + if not self.es_based: + return super(BaseACL, self).__getitem__(key) + return self.getitem_es(self.item_db_id(key)) + + def getitem_es(self, key): + es = ES(self.item_model.__name__) + obj = es.get_item(id=key) + obj.__acl__ = self.item_acl(obj) + obj.__parent__ = self + obj.__name__ = key + return obj + + +class DatabaseACLMixin(object): + """ Mixin to be used when ACLs are stored in database. """ + + def item_acl(self, item): + """ Objectify ACL if ES is used or call item.get_acl() if + db is used. + """ + if self.es_based: + from nefertari_guards.elasticsearch import get_es_item_acl + return get_es_item_acl(item) + return super(DatabaseACLMixin, self).item_acl(item) + + def getitem_es(self, key): + """ Override to support ACL filtering. + + To do so: passes `self.request` to `get_item` and uses + `ACLFilterES`. + """ + from nefertari_guards.elasticsearch import ACLFilterES + es = ACLFilterES(self.item_model.__name__) + params = { + 'id': key, + 'request': self.request, + } + obj = es.get_item(**params) + obj.__acl__ = self.item_acl(obj) + obj.__parent__ = self + obj.__name__ = key + return obj +