diff --git a/geonode/security/tasks.py b/geonode/security/tasks.py index 026348eca81..bc53c77eb4d 100644 --- a/geonode/security/tasks.py +++ b/geonode/security/tasks.py @@ -1,83 +1,86 @@ -# -*- coding: utf-8 -*- -######################################################################### -# -# Copyright (C) 2018 OSGeo -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -######################################################################### - -import logging -from celery import shared_task - -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group - -from geonode.layers.models import Layer -from geonode.geoserver.tasks import thumbnail_task - -from .utils import (purge_geofence_layer_rules, - sync_geofence_with_guardian) # set_geofence_invalidate_cache - -logger = logging.getLogger(__name__) - - -def _log(msg, *args): - logger.debug(msg, *args) - - -@shared_task -def synch_guardian(): - from geonode.base.models import ResourceBase - dirty_resources = ResourceBase.objects.filter(dirty_state=True) - if dirty_resources and dirty_resources.count() > 0: - _log(" --------------------------- synching with guardian!") - for r in dirty_resources: - if r.polymorphic_ctype.name == 'layer': - layer = None - try: - purge_geofence_layer_rules(r) - layer = Layer.objects.get(id=r.id) - perm_spec = layer.get_all_level_info() - _log(" %s --------------------------- %s " % (layer, perm_spec)) - - # All the other users - if 'users' in perm_spec: - for user, perms in perm_spec['users'].items(): - user = get_user_model().objects.get(username=user) - # Set the GeoFence User Rules - geofence_user = str(user) - if "AnonymousUser" in geofence_user: - geofence_user = None - sync_geofence_with_guardian(layer, perms, user=geofence_user) - - # All the other groups - if 'groups' in perm_spec: - for group, perms in perm_spec['groups'].items(): - group = Group.objects.get(name=group) - # Set the GeoFence Group Rules - sync_geofence_with_guardian(layer, perms, group=group) - - try: - thumbnail_task.delay(layer, overwrite=True, check_bbox=True) - except BaseException: - logger.warn("!WARNING! - Failure while Creating Thumbnail for Layer [%s]" % (layer.alternate)) - - r.clear_dirty_state() - except BaseException: - logger.warn("!WARNING! - Failure Synching-up Security Rules for Resource [%s]" % (r)) - - # if set_geofence_invalidate_cache(): - # for r in dirty_resources: - # _log(" --------------------------- clearing %s" % r) +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2018 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +import logging +from celery import shared_task + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group + +from geonode.layers.models import Layer +from geonode.geoserver.tasks import thumbnail_task + +from .utils import (purge_geofence_layer_rules, + sync_geofence_with_guardian) # set_geofence_invalidate_cache + +logger = logging.getLogger(__name__) + + +def _log(msg, *args): + logger.debug(msg, *args) + + +@shared_task +def synch_guardian(): + if getattr(settings, 'DELAYED_SECURITY_SIGNALS', False): + from geonode.base.models import ResourceBase + dirty_resources = ResourceBase.objects.filter(dirty_state=True) + if dirty_resources and dirty_resources.count() > 0: + _log(" --------------------------- synching with guardian!") + for r in dirty_resources: + if r.polymorphic_ctype.name == 'layer': + layer = None + try: + purge_geofence_layer_rules(r) + layer = Layer.objects.get(id=r.id) + perm_spec = layer.get_all_level_info() + _log(" %s --------------------------- %s " % (layer, perm_spec)) + + # All the other users + if 'users' in perm_spec: + for user, perms in perm_spec['users'].items(): + user = get_user_model().objects.get(username=user) + # Set the GeoFence User Rules + geofence_user = str(user) + if "AnonymousUser" in geofence_user: + geofence_user = None + sync_geofence_with_guardian(layer, perms, user=geofence_user) + + # All the other groups + if 'groups' in perm_spec: + for group, perms in perm_spec['groups'].items(): + group = Group.objects.get(name=group) + # Set the GeoFence Group Rules + sync_geofence_with_guardian(layer, perms, group=group) + + try: + thumbnail_task.delay(layer, overwrite=True, check_bbox=True) + except BaseException: + logger.warn("!WARNING! - Failure while Creating Thumbnail \ + for Layer [%s]" % (layer.alternate)) + + r.clear_dirty_state() + except BaseException: + logger.warn("!WARNING! - Failure Synching-up Security Rules for Resource [%s]" % (r)) + + # if set_geofence_invalidate_cache(): + # for r in dirty_resources: + # _log(" --------------------------- clearing %s" % r) diff --git a/geonode/security/utils.py b/geonode/security/utils.py index 63edc77aaf6..43ce5cf7dc8 100644 --- a/geonode/security/utils.py +++ b/geonode/security/utils.py @@ -1,606 +1,606 @@ -# -*- coding: utf-8 -*- -######################################################################### -# -# Copyright (C) 2018 OSGeo -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -######################################################################### -from geonode import geoserver -from geonode.decorators import on_ogc_backend -from lxml import etree - -try: - import json -except ImportError: - from django.utils import simplejson as json -import logging -import traceback -import requests -import models - -from requests.auth import HTTPBasicAuth -from django.conf import settings -from django.db.models import Q -from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType -# from django.contrib.auth import login -from django.contrib.auth.models import Group, Permission -from django.core.exceptions import ObjectDoesNotExist -from guardian.utils import get_user_obj_perms_model -from guardian.shortcuts import assign_perm -from geonode.groups.models import GroupProfile -from ..services.enumerations import CASCADED - -logger = logging.getLogger("geonode.security.utils") - - -def get_visible_resources(queryset, - user, - admin_approval_required=False, - unpublished_not_visible=False, - private_groups_not_visibile=False): - is_admin = False - is_manager = False - if user: - is_admin = user.is_superuser if user else False - try: - is_manager = user.groupmember_set.all().filter(role='manager').exists() - except BaseException: - is_manager = False - - # Get the list of objects the user has access to - anonymous_group = None - public_groups = GroupProfile.objects.exclude(access="private").values('group') - groups = [] - group_list_all = [] - manager_groups = [] - try: - group_list_all = user.group_list_all().values('group') - except BaseException: - pass - try: - manager_groups = Group.objects.filter( - name__in=user.groupmember_set.filter(role="manager").values_list("group__slug", flat=True)) - except BaseException: - pass - try: - anonymous_group = Group.objects.get(name='anonymous') - if anonymous_group and anonymous_group not in groups: - groups.append(anonymous_group) - except BaseException: - pass - - filter_set = queryset - - if admin_approval_required: - if not is_admin: - if is_manager: - filter_set = filter_set.filter( - Q(is_published=True) | - Q(group__in=groups) | - Q(group__in=manager_groups) | - Q(group__in=group_list_all) | - Q(group__in=public_groups) | - Q(owner__username__iexact=str(user))) - elif user: - filter_set = filter_set.filter( - Q(is_published=True) | - Q(group__in=groups) | - Q(group__in=group_list_all) | - Q(group__in=public_groups) | - Q(owner__username__iexact=str(user))) - else: - filter_set = filter_set.filter( - Q(is_published=True) | - Q(group__in=public_groups) | - Q(group__in=groups)) - - if unpublished_not_visible: - if not is_admin: - if user: - filter_set = filter_set.exclude( - Q(is_published=False) & ~( - Q(owner__username__iexact=str(user)) | Q(group__in=group_list_all))) - else: - filter_set = filter_set.exclude(Q(is_published=False)) - - if private_groups_not_visibile: - if not is_admin: - private_groups = GroupProfile.objects.filter(access="private").values('group') - if user: - filter_set = filter_set.exclude( - Q(group__in=private_groups) & ~( - Q(owner__username__iexact=str(user)) | Q(group__in=group_list_all))) - else: - filter_set = filter_set.exclude(group__in=private_groups) - - # Hide Dirty State Resources - if not is_admin: - if user: - filter_set = filter_set.exclude( - Q(dirty_state=True) & ~( - Q(owner__username__iexact=str(user)) | Q(group__in=group_list_all))) - else: - filter_set = filter_set.exclude(Q(dirty_state=True)) - - return filter_set - - -def get_users_with_perms(obj): - """ - Override of the Guardian get_users_with_perms - """ - ctype = ContentType.objects.get_for_model(obj) - permissions = {} - PERMISSIONS_TO_FETCH = models.ADMIN_PERMISSIONS + models.LAYER_ADMIN_PERMISSIONS - - for perm in Permission.objects.filter(codename__in=PERMISSIONS_TO_FETCH, content_type_id=ctype.id): - permissions[perm.id] = perm.codename - - user_model = get_user_obj_perms_model(obj) - users_with_perms = user_model.objects.filter(object_pk=obj.pk, - content_type_id=ctype.id, - permission_id__in=permissions).values('user_id', 'permission_id') - - users = {} - for item in users_with_perms: - if item['user_id'] in users: - users[item['user_id']].append(permissions[item['permission_id']]) - else: - users[item['user_id']] = [permissions[item['permission_id']], ] - - profiles = {} - for profile in get_user_model().objects.filter(id__in=users.keys()): - profiles[profile] = users[profile.id] - - return profiles - - -@on_ogc_backend(geoserver.BACKEND_PACKAGE) -def get_geofence_rules_count(): - """Get the number of available GeoFence Cache Rules""" - try: - url = settings.OGC_SERVER['default']['LOCATION'] - user = settings.OGC_SERVER['default']['USER'] - passwd = settings.OGC_SERVER['default']['PASSWORD'] - # Check first that the rules does not exist already - """ - curl -X GET -u admin:geoserver \ - http://:/geoserver/rest/geofence/rules/count.json - """ - headers = {'Content-type': 'application/json'} - r = requests.get(url + 'rest/geofence/rules/count.json', - headers=headers, - auth=HTTPBasicAuth(user, passwd)) - if (r.status_code < 200 or r.status_code > 201): - logger.warning("Could not retrieve GeoFence Rules count.") - - rules_objs = json.loads(r.text) - rules_count = rules_objs['count'] - return int(rules_count) - except BaseException: - tb = traceback.format_exc() - logger.debug(tb) - return -1 - - -@on_ogc_backend(geoserver.BACKEND_PACKAGE) -def get_highest_priority(): - """Get the highest Rules priority""" - try: - rules_count = get_geofence_rules_count() - - url = settings.OGC_SERVER['default']['LOCATION'] - user = settings.OGC_SERVER['default']['USER'] - passwd = settings.OGC_SERVER['default']['PASSWORD'] - # Check first that the rules does not exist already - """ - curl -X GET -u admin:geoserver \ - http://:/geoserver/rest/geofence/rules.json?page=(count-1)&entries=1 - """ - headers = {'Content-type': 'application/json'} - r = requests.get(url + 'rest/geofence/rules.json?page=' + str(rules_count-1) + '&entries=1', - headers=headers, - auth=HTTPBasicAuth(user, passwd)) - if (r.status_code < 200 or r.status_code > 201): - logger.warning("Could not retrieve GeoFence Rules count.") - - rules_objs = json.loads(r.text) - if len(rules_objs['rules']) > 0: - highest_priority = rules_objs['rules'][0]['priority'] - else: - highest_priority = 0 - return int(highest_priority) - except BaseException: - tb = traceback.format_exc() - logger.debug(tb) - return -1 - - -@on_ogc_backend(geoserver.BACKEND_PACKAGE) -def purge_geofence_all(): - """purge all existing GeoFence Cache Rules""" - if settings.OGC_SERVER['default']['GEOFENCE_SECURITY_ENABLED']: - try: - url = settings.OGC_SERVER['default']['LOCATION'] - user = settings.OGC_SERVER['default']['USER'] - passwd = settings.OGC_SERVER['default']['PASSWORD'] - """ - curl -X GET -u admin:geoserver -H "Content-Type: application/json" \ - http://:/geoserver/rest/geofence/rules.json - """ - headers = {'Content-type': 'application/json'} - r = requests.get(url + 'rest/geofence/rules.json', - headers=headers, - auth=HTTPBasicAuth(user, passwd)) - if (r.status_code < 200 or r.status_code > 201): - logger.warning("Could not Retrieve GeoFence Rules") - else: - try: - rules_objs = json.loads(r.text) - rules_count = rules_objs['count'] - rules = rules_objs['rules'] - if rules_count > 0: - # Delete GeoFence Rules associated to the Layer - # curl -X DELETE -u admin:geoserver http://:/geoserver/rest/geofence/rules/id/{r_id} - for i, rule in enumerate(rules): - r = requests.delete(url + 'rest/geofence/rules/id/' + str(rule['id']), - headers=headers, - auth=HTTPBasicAuth(user, passwd)) - if (r.status_code < 200 or r.status_code > 201): - msg = "Could not DELETE GeoServer Rule id[%s]" % rule['id'] - e = Exception(msg) - logger.debug("Response [{}] : {}".format(r.status_code, r.text)) - raise e - except Exception: - logger.debug("Response [{}] : {}".format(r.status_code, r.text)) - except BaseException: - tb = traceback.format_exc() - logger.debug(tb) - - -@on_ogc_backend(geoserver.BACKEND_PACKAGE) -def purge_geofence_layer_rules(resource): - """purge layer existing GeoFence Cache Rules""" - # Scan GeoFence Rules associated to the Layer - """ - curl -u admin:geoserver - http://:/geoserver/rest/geofence/rules.json?workspace=geonode&layer={layer} - """ - url = settings.OGC_SERVER['default']['LOCATION'] - user = settings.OGC_SERVER['default']['USER'] - passwd = settings.OGC_SERVER['default']['PASSWORD'] - headers = {'Content-type': 'application/json'} - workspace = _get_layer_workspace(resource.layer) - r = requests.get( - "{}rest/geofence/rules.json?workspace={}&layer={}".format( - url, workspace, resource.layer.name), - headers=headers, - auth=HTTPBasicAuth(user, passwd) - ) - if (r.status_code >= 200 and r.status_code < 300): - gs_rules = r.json() - r_ids = [] - if gs_rules and gs_rules['rules']: - for r in gs_rules['rules']: - if r['layer'] and r['layer'] == resource.layer.name: - r_ids.append(r['id']) - - # Delete GeoFence Rules associated to the Layer - # curl -X DELETE -u admin:geoserver http://:/geoserver/rest/geofence/rules/id/{r_id} - for i, r_id in enumerate(r_ids): - r = requests.delete(url + 'rest/geofence/rules/id/' + str(r_id), - headers=headers, - auth=HTTPBasicAuth(user, passwd)) - if (r.status_code < 200 or r.status_code > 201): - msg = "Could not DELETE GeoServer Rule for Layer " - msg = msg + str(resource.layer.name) - e = Exception(msg) - logger.debug("Response [{}] : {}".format(r.status_code, r.text)) - raise e - - -@on_ogc_backend(geoserver.BACKEND_PACKAGE) -def set_geofence_invalidate_cache(): - """invalidate GeoFence Cache Rules""" - if settings.OGC_SERVER['default']['GEOFENCE_SECURITY_ENABLED']: - try: - url = settings.OGC_SERVER['default']['LOCATION'] - user = settings.OGC_SERVER['default']['USER'] - passwd = settings.OGC_SERVER['default']['PASSWORD'] - # Check first that the rules does not exist already - """ - curl -X GET -u admin:geoserver \ - http://:/geoserver/rest/ruleCache/invalidate - """ - r = requests.put(url + 'rest/ruleCache/invalidate', - auth=HTTPBasicAuth(user, passwd)) - - if (r.status_code < 200 or r.status_code > 201): - logger.warning("Could not Invalidate GeoFence Rules.") - return False - return True - except BaseException: - tb = traceback.format_exc() - logger.debug(tb) - return False - - -@on_ogc_backend(geoserver.BACKEND_PACKAGE) -def set_geowebcache_invalidate_cache(layer_alternate): - """invalidate GeoWebCache Cache Rules""" - try: - url = settings.OGC_SERVER['default']['LOCATION'] - user = settings.OGC_SERVER['default']['USER'] - passwd = settings.OGC_SERVER['default']['PASSWORD'] - # Check first that the rules does not exist already - """ - curl -v -u admin:geoserver \ - -H "Content-type: text/xml" \ - -d "{layer_alternate}" \ - http://localhost:8080/geoserver/gwc/rest/masstruncate - """ - headers = {'Content-type': 'text/xml'} - payload = "%s" % layer_alternate - r = requests.post(url + 'gwc/rest/masstruncate', - headers=headers, - data=payload, - auth=HTTPBasicAuth(user, passwd)) - if (r.status_code < 200 or r.status_code > 201): - logger.warning("Could not Truncate GWC Cache for Layer '%s'." % layer_alternate) - except BaseException: - tb = traceback.format_exc() - logger.debug(tb) - - -@on_ogc_backend(geoserver.BACKEND_PACKAGE) -def set_geofence_all(instance): - """assign access permissions to all users - - This method is only relevant to Layer instances that have their - underlying data managed by geoserver, meaning: - - * layers that are not associated with a Service - * layers that are associated with a Service that is being CASCADED through - geoserver - - """ - - resource = instance.get_self_resource() - logger.debug("Inside set_geofence_all for instance {}".format(instance)) - try: - workspace = _get_layer_workspace(resource.layer) - logger.debug("going to work in workspace {!r}".format(workspace)) - except (ObjectDoesNotExist, AttributeError, RuntimeError): - # This is either not a layer (if raised AttributeError) or it is - # a layer that is not manageable by geofence (if raised - # RuntimeError) so we have nothing to do - return - try: - url = settings.OGC_SERVER['default']['LOCATION'] - user = settings.OGC_SERVER['default']['USER'] - passwd = settings.OGC_SERVER['default']['PASSWORD'] - - # Create GeoFence Rules for ANONYMOUS to the Layer - """ - curl -X POST -u admin:geoserver -H "Content-Type: text/xml" -d \ - "geonode{layer}ALLOW" \ - http://:/geoserver/rest/geofence/rules - """ - headers = {'Content-type': 'application/xml'} - payload = _get_geofence_payload( - layer=resource.layer.name, - workspace=workspace, - access="ALLOW" - ) - response = requests.post( - url + 'rest/geofence/rules', - headers=headers, - data=payload, - auth=HTTPBasicAuth(user, passwd) - ) - if response.status_code not in (200, 201): - logger.debug( - "Response {!r} : {}".format(response.status_code, response.text)) - raise RuntimeError("Could not ADD GeoServer ANONYMOUS Rule " - "for Layer {}".format(resource.layer.name)) - except BaseException: - tb = traceback.format_exc() - logger.debug(tb) - finally: - if not settings.DELAYED_SECURITY_SIGNALS: - set_geofence_invalidate_cache() - else: - resource.set_dirty_state() - - -@on_ogc_backend(geoserver.BACKEND_PACKAGE) -def sync_geofence_with_guardian(layer, perms, user=None, group=None): - """ - Sync Guardian permissions to GeoFence. - """ - # Create new rule-set - gf_services = {} - gf_services["*"] = 'download_resourcebase' in perms and \ - ('view_resourcebase' in perms or 'change_layer_style' in perms) - gf_services["WMS"] = 'view_resourcebase' in perms or 'change_layer_style' in perms - gf_services["GWC"] = 'view_resourcebase' in perms or 'change_layer_style' in perms - gf_services["WFS"] = ('download_resourcebase' in perms or 'change_layer_data' in perms) \ - and layer.is_vector() - gf_services["WCS"] = ('download_resourcebase' in perms or 'change_layer_data' in perms) \ - and not layer.is_vector() - gf_services["WPS"] = 'download_resourcebase' in perms or 'change_layer_data' in perms - - _user = None - if user: - _user = user if isinstance(user, basestring) else user.username - _group = None - if group: - _group = group if isinstance(group, basestring) else group.name - for service, allowed in gf_services.iteritems(): - if allowed: - if _user: - logger.debug("Adding 'user' to geofence the rule: %s %s %s" % (layer, service, _user)) - _update_geofence_rule(layer.name, layer.workspace, service, user=_user) - elif not _group: - logger.debug("Adding to geofence the rule: %s %s *" % (layer, service)) - _update_geofence_rule(layer.name, layer.workspace, service) - - if _group: - logger.debug("Adding 'group' to geofence the rule: %s %s %s" % (layer, service, _group)) - _update_geofence_rule(layer.name, layer.workspace, service, group=_group) - if not settings.DELAYED_SECURITY_SIGNALS: - set_geofence_invalidate_cache() - else: - layer.set_dirty_state() - - -def set_owner_permissions(resource): - """assign all admin permissions to the owner""" - if resource.polymorphic_ctype: - # Set the GeoFence Owner Rule - if resource.polymorphic_ctype.name == 'layer': - for perm in models.LAYER_ADMIN_PERMISSIONS: - assign_perm(perm, resource.owner, resource.layer) - for perm in models.ADMIN_PERMISSIONS: - assign_perm(perm, resource.owner, resource.get_self_resource()) - - -def remove_object_permissions(instance): - """Remove object permissions on given resource. - - If is a layer removes the layer specific permissions then the - resourcebase permissions - - """ - - from guardian.models import UserObjectPermission, GroupObjectPermission - resource = instance.get_self_resource() - if hasattr(resource, "layer"): - try: - UserObjectPermission.objects.filter( - content_type=ContentType.objects.get_for_model(resource.layer), - object_pk=instance.id - ).delete() - GroupObjectPermission.objects.filter( - content_type=ContentType.objects.get_for_model(resource.layer), - object_pk=instance.id - ).delete() - if settings.OGC_SERVER['default']['GEOFENCE_SECURITY_ENABLED']: - purge_geofence_layer_rules(resource) - except (ObjectDoesNotExist, RuntimeError): - pass # This layer is not manageable by geofence - except BaseException: - tb = traceback.format_exc() - logger.debug(tb) - finally: - if not settings.DELAYED_SECURITY_SIGNALS: - set_geofence_invalidate_cache() - else: - resource.set_dirty_state() - - UserObjectPermission.objects.filter(content_type=ContentType.objects.get_for_model(resource), - object_pk=instance.id).delete() - GroupObjectPermission.objects.filter(content_type=ContentType.objects.get_for_model(resource), - object_pk=instance.id).delete() - - -# # Logic to login a user automatically when it has successfully -# # activated an account: -# def autologin(sender, **kwargs): -# user = kwargs['user'] -# request = kwargs['request'] -# # Manually setting the default user backed to avoid the -# # 'User' object has no attribute 'backend' error -# user.backend = 'django.contrib.auth.backends.ModelBackend' -# # This login function does not need password. -# login(request, user) -# -# # FIXME(Ariel): Replace this signal with the one from django-user-accounts -# # user_activated.connect(autologin) - - -def _get_layer_workspace(layer): - """Get the workspace where the input layer belongs""" - workspace = layer.workspace - if not workspace: - default_workspace = getattr(settings, "DEFAULT_WORKSPACE", "geonode") - try: - if layer.remote_service.method == CASCADED: - workspace = getattr( - settings, "CASCADE_WORKSPACE", default_workspace) - else: - raise RuntimeError("Layer is not cascaded") - except AttributeError: # layer does not have a service - workspace = default_workspace - return workspace - - -def _get_geofence_payload(layer, workspace, access, user=None, group=None, - service=None): - highest_priority = get_highest_priority() - root_el = etree.Element("Rule") - username_el = etree.SubElement(root_el, "userName") - if user is not None: - username_el.text = user - else: - username_el.text = '' - priority_el = etree.SubElement(root_el, "priority") - priority_el.text = str(highest_priority if highest_priority >= 0 else 0) - if group is not None: - role_el = etree.SubElement(root_el, "roleName") - role_el.text = "ROLE_{}".format(group.upper()) - workspace_el = etree.SubElement(root_el, "workspace") - workspace_el.text = workspace - layer_el = etree.SubElement(root_el, "layer") - layer_el.text = layer - access_el = etree.SubElement(root_el, "access") - access_el.text = access - if service is not None and service is not "*": - service_el = etree.SubElement(root_el, "service") - service_el.text = service - return etree.tostring(root_el) - - -def _update_geofence_rule(layer, workspace, service, user=None, group=None): - payload = _get_geofence_payload( - layer=layer, - workspace=workspace, - access="ALLOW", - user=user, - group=group, - service=service - ) - logger.debug("request data: {}".format(payload)) - response = requests.post( - "{base_url}rest/geofence/rules".format( - base_url=settings.OGC_SERVER['default']['LOCATION']), - data=payload, - headers={ - 'Content-type': 'application/xml' - }, - auth=HTTPBasicAuth( - username=settings.OGC_SERVER['default']['USER'], - password=settings.OGC_SERVER['default']['PASSWORD'] - ) - ) - logger.debug("response status_code: {}".format(response.status_code)) - if response.status_code not in (200, 201): - msg = ("Could not ADD GeoServer User {!r} Rule for " - "Layer {!r}: '{!r}'".format(user, layer, response.text)) - if 'Duplicate Rule' in response.text: - logger.warning(msg) - else: - raise RuntimeError(msg) +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2018 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from geonode import geoserver +from geonode.decorators import on_ogc_backend +from lxml import etree + +try: + import json +except ImportError: + from django.utils import simplejson as json +import logging +import traceback +import requests +import models + +from requests.auth import HTTPBasicAuth +from django.conf import settings +from django.db.models import Q +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType +# from django.contrib.auth import login +from django.contrib.auth.models import Group, Permission +from django.core.exceptions import ObjectDoesNotExist +from guardian.utils import get_user_obj_perms_model +from guardian.shortcuts import assign_perm +from geonode.groups.models import GroupProfile +from ..services.enumerations import CASCADED + +logger = logging.getLogger("geonode.security.utils") + + +def get_visible_resources(queryset, + user, + admin_approval_required=False, + unpublished_not_visible=False, + private_groups_not_visibile=False): + is_admin = False + is_manager = False + if user: + is_admin = user.is_superuser if user else False + try: + is_manager = user.groupmember_set.all().filter(role='manager').exists() + except BaseException: + is_manager = False + + # Get the list of objects the user has access to + anonymous_group = None + public_groups = GroupProfile.objects.exclude(access="private").values('group') + groups = [] + group_list_all = [] + manager_groups = [] + try: + group_list_all = user.group_list_all().values('group') + except BaseException: + pass + try: + manager_groups = Group.objects.filter( + name__in=user.groupmember_set.filter(role="manager").values_list("group__slug", flat=True)) + except BaseException: + pass + try: + anonymous_group = Group.objects.get(name='anonymous') + if anonymous_group and anonymous_group not in groups: + groups.append(anonymous_group) + except BaseException: + pass + + filter_set = queryset + + if admin_approval_required: + if not is_admin: + if is_manager: + filter_set = filter_set.filter( + Q(is_published=True) | + Q(group__in=groups) | + Q(group__in=manager_groups) | + Q(group__in=group_list_all) | + Q(group__in=public_groups) | + Q(owner__username__iexact=str(user))) + elif user: + filter_set = filter_set.filter( + Q(is_published=True) | + Q(group__in=groups) | + Q(group__in=group_list_all) | + Q(group__in=public_groups) | + Q(owner__username__iexact=str(user))) + else: + filter_set = filter_set.filter( + Q(is_published=True) | + Q(group__in=public_groups) | + Q(group__in=groups)) + + if unpublished_not_visible: + if not is_admin: + if user: + filter_set = filter_set.exclude( + Q(is_published=False) & ~( + Q(owner__username__iexact=str(user)) | Q(group__in=group_list_all))) + else: + filter_set = filter_set.exclude(Q(is_published=False)) + + if private_groups_not_visibile: + if not is_admin: + private_groups = GroupProfile.objects.filter(access="private").values('group') + if user: + filter_set = filter_set.exclude( + Q(group__in=private_groups) & ~( + Q(owner__username__iexact=str(user)) | Q(group__in=group_list_all))) + else: + filter_set = filter_set.exclude(group__in=private_groups) + + # Hide Dirty State Resources + if not is_admin: + if user: + filter_set = filter_set.exclude( + Q(dirty_state=True) & ~( + Q(owner__username__iexact=str(user)) | Q(group__in=group_list_all))) + else: + filter_set = filter_set.exclude(Q(dirty_state=True)) + + return filter_set + + +def get_users_with_perms(obj): + """ + Override of the Guardian get_users_with_perms + """ + ctype = ContentType.objects.get_for_model(obj) + permissions = {} + PERMISSIONS_TO_FETCH = models.ADMIN_PERMISSIONS + models.LAYER_ADMIN_PERMISSIONS + + for perm in Permission.objects.filter(codename__in=PERMISSIONS_TO_FETCH, content_type_id=ctype.id): + permissions[perm.id] = perm.codename + + user_model = get_user_obj_perms_model(obj) + users_with_perms = user_model.objects.filter(object_pk=obj.pk, + content_type_id=ctype.id, + permission_id__in=permissions).values('user_id', 'permission_id') + + users = {} + for item in users_with_perms: + if item['user_id'] in users: + users[item['user_id']].append(permissions[item['permission_id']]) + else: + users[item['user_id']] = [permissions[item['permission_id']], ] + + profiles = {} + for profile in get_user_model().objects.filter(id__in=users.keys()): + profiles[profile] = users[profile.id] + + return profiles + + +@on_ogc_backend(geoserver.BACKEND_PACKAGE) +def get_geofence_rules_count(): + """Get the number of available GeoFence Cache Rules""" + try: + url = settings.OGC_SERVER['default']['LOCATION'] + user = settings.OGC_SERVER['default']['USER'] + passwd = settings.OGC_SERVER['default']['PASSWORD'] + # Check first that the rules does not exist already + """ + curl -X GET -u admin:geoserver \ + http://:/geoserver/rest/geofence/rules/count.json + """ + headers = {'Content-type': 'application/json'} + r = requests.get(url + 'rest/geofence/rules/count.json', + headers=headers, + auth=HTTPBasicAuth(user, passwd)) + if (r.status_code < 200 or r.status_code > 201): + logger.warning("Could not retrieve GeoFence Rules count.") + + rules_objs = json.loads(r.text) + rules_count = rules_objs['count'] + return int(rules_count) + except BaseException: + tb = traceback.format_exc() + logger.debug(tb) + return -1 + + +@on_ogc_backend(geoserver.BACKEND_PACKAGE) +def get_highest_priority(): + """Get the highest Rules priority""" + try: + rules_count = get_geofence_rules_count() + + url = settings.OGC_SERVER['default']['LOCATION'] + user = settings.OGC_SERVER['default']['USER'] + passwd = settings.OGC_SERVER['default']['PASSWORD'] + # Check first that the rules does not exist already + """ + curl -X GET -u admin:geoserver \ + http://:/geoserver/rest/geofence/rules.json?page=(count-1)&entries=1 + """ + headers = {'Content-type': 'application/json'} + r = requests.get(url + 'rest/geofence/rules.json?page=' + str(rules_count-1) + '&entries=1', + headers=headers, + auth=HTTPBasicAuth(user, passwd)) + if (r.status_code < 200 or r.status_code > 201): + logger.warning("Could not retrieve GeoFence Rules count.") + + rules_objs = json.loads(r.text) + if len(rules_objs['rules']) > 0: + highest_priority = rules_objs['rules'][0]['priority'] + else: + highest_priority = 0 + return int(highest_priority) + except BaseException: + tb = traceback.format_exc() + logger.debug(tb) + return -1 + + +@on_ogc_backend(geoserver.BACKEND_PACKAGE) +def purge_geofence_all(): + """purge all existing GeoFence Cache Rules""" + if settings.OGC_SERVER['default']['GEOFENCE_SECURITY_ENABLED']: + try: + url = settings.OGC_SERVER['default']['LOCATION'] + user = settings.OGC_SERVER['default']['USER'] + passwd = settings.OGC_SERVER['default']['PASSWORD'] + """ + curl -X GET -u admin:geoserver -H "Content-Type: application/json" \ + http://:/geoserver/rest/geofence/rules.json + """ + headers = {'Content-type': 'application/json'} + r = requests.get(url + 'rest/geofence/rules.json', + headers=headers, + auth=HTTPBasicAuth(user, passwd)) + if (r.status_code < 200 or r.status_code > 201): + logger.warning("Could not Retrieve GeoFence Rules") + else: + try: + rules_objs = json.loads(r.text) + rules_count = rules_objs['count'] + rules = rules_objs['rules'] + if rules_count > 0: + # Delete GeoFence Rules associated to the Layer + # curl -X DELETE -u admin:geoserver http://:/geoserver/rest/geofence/rules/id/{r_id} + for i, rule in enumerate(rules): + r = requests.delete(url + 'rest/geofence/rules/id/' + str(rule['id']), + headers=headers, + auth=HTTPBasicAuth(user, passwd)) + if (r.status_code < 200 or r.status_code > 201): + msg = "Could not DELETE GeoServer Rule id[%s]" % rule['id'] + e = Exception(msg) + logger.debug("Response [{}] : {}".format(r.status_code, r.text)) + raise e + except Exception: + logger.debug("Response [{}] : {}".format(r.status_code, r.text)) + except BaseException: + tb = traceback.format_exc() + logger.debug(tb) + + +@on_ogc_backend(geoserver.BACKEND_PACKAGE) +def purge_geofence_layer_rules(resource): + """purge layer existing GeoFence Cache Rules""" + # Scan GeoFence Rules associated to the Layer + """ + curl -u admin:geoserver + http://:/geoserver/rest/geofence/rules.json?workspace=geonode&layer={layer} + """ + url = settings.OGC_SERVER['default']['LOCATION'] + user = settings.OGC_SERVER['default']['USER'] + passwd = settings.OGC_SERVER['default']['PASSWORD'] + headers = {'Content-type': 'application/json'} + workspace = _get_layer_workspace(resource.layer) + r = requests.get( + "{}rest/geofence/rules.json?workspace={}&layer={}".format( + url, workspace, resource.layer.name), + headers=headers, + auth=HTTPBasicAuth(user, passwd) + ) + if (r.status_code >= 200 and r.status_code < 300): + gs_rules = r.json() + r_ids = [] + if gs_rules and gs_rules['rules']: + for r in gs_rules['rules']: + if r['layer'] and r['layer'] == resource.layer.name: + r_ids.append(r['id']) + + # Delete GeoFence Rules associated to the Layer + # curl -X DELETE -u admin:geoserver http://:/geoserver/rest/geofence/rules/id/{r_id} + for i, r_id in enumerate(r_ids): + r = requests.delete(url + 'rest/geofence/rules/id/' + str(r_id), + headers=headers, + auth=HTTPBasicAuth(user, passwd)) + if (r.status_code < 200 or r.status_code > 201): + msg = "Could not DELETE GeoServer Rule for Layer " + msg = msg + str(resource.layer.name) + e = Exception(msg) + logger.debug("Response [{}] : {}".format(r.status_code, r.text)) + raise e + + +@on_ogc_backend(geoserver.BACKEND_PACKAGE) +def set_geofence_invalidate_cache(): + """invalidate GeoFence Cache Rules""" + if settings.OGC_SERVER['default']['GEOFENCE_SECURITY_ENABLED']: + try: + url = settings.OGC_SERVER['default']['LOCATION'] + user = settings.OGC_SERVER['default']['USER'] + passwd = settings.OGC_SERVER['default']['PASSWORD'] + # Check first that the rules does not exist already + """ + curl -X GET -u admin:geoserver \ + http://:/geoserver/rest/ruleCache/invalidate + """ + r = requests.put(url + 'rest/ruleCache/invalidate', + auth=HTTPBasicAuth(user, passwd)) + + if (r.status_code < 200 or r.status_code > 201): + logger.warning("Could not Invalidate GeoFence Rules.") + return False + return True + except BaseException: + tb = traceback.format_exc() + logger.debug(tb) + return False + + +@on_ogc_backend(geoserver.BACKEND_PACKAGE) +def set_geowebcache_invalidate_cache(layer_alternate): + """invalidate GeoWebCache Cache Rules""" + try: + url = settings.OGC_SERVER['default']['LOCATION'] + user = settings.OGC_SERVER['default']['USER'] + passwd = settings.OGC_SERVER['default']['PASSWORD'] + # Check first that the rules does not exist already + """ + curl -v -u admin:geoserver \ + -H "Content-type: text/xml" \ + -d "{layer_alternate}" \ + http://localhost:8080/geoserver/gwc/rest/masstruncate + """ + headers = {'Content-type': 'text/xml'} + payload = "%s" % layer_alternate + r = requests.post(url + 'gwc/rest/masstruncate', + headers=headers, + data=payload, + auth=HTTPBasicAuth(user, passwd)) + if (r.status_code < 200 or r.status_code > 201): + logger.warning("Could not Truncate GWC Cache for Layer '%s'." % layer_alternate) + except BaseException: + tb = traceback.format_exc() + logger.debug(tb) + + +@on_ogc_backend(geoserver.BACKEND_PACKAGE) +def set_geofence_all(instance): + """assign access permissions to all users + + This method is only relevant to Layer instances that have their + underlying data managed by geoserver, meaning: + + * layers that are not associated with a Service + * layers that are associated with a Service that is being CASCADED through + geoserver + + """ + + resource = instance.get_self_resource() + logger.debug("Inside set_geofence_all for instance {}".format(instance)) + try: + workspace = _get_layer_workspace(resource.layer) + logger.debug("going to work in workspace {!r}".format(workspace)) + except (ObjectDoesNotExist, AttributeError, RuntimeError): + # This is either not a layer (if raised AttributeError) or it is + # a layer that is not manageable by geofence (if raised + # RuntimeError) so we have nothing to do + return + try: + url = settings.OGC_SERVER['default']['LOCATION'] + user = settings.OGC_SERVER['default']['USER'] + passwd = settings.OGC_SERVER['default']['PASSWORD'] + + # Create GeoFence Rules for ANONYMOUS to the Layer + """ + curl -X POST -u admin:geoserver -H "Content-Type: text/xml" -d \ + "geonode{layer}ALLOW" \ + http://:/geoserver/rest/geofence/rules + """ + headers = {'Content-type': 'application/xml'} + payload = _get_geofence_payload( + layer=resource.layer.name, + workspace=workspace, + access="ALLOW" + ) + response = requests.post( + url + 'rest/geofence/rules', + headers=headers, + data=payload, + auth=HTTPBasicAuth(user, passwd) + ) + if response.status_code not in (200, 201): + logger.debug( + "Response {!r} : {}".format(response.status_code, response.text)) + raise RuntimeError("Could not ADD GeoServer ANONYMOUS Rule " + "for Layer {}".format(resource.layer.name)) + except BaseException: + tb = traceback.format_exc() + logger.debug(tb) + finally: + if not getattr(settings, 'DELAYED_SECURITY_SIGNALS', False): + set_geofence_invalidate_cache() + else: + resource.set_dirty_state() + + +@on_ogc_backend(geoserver.BACKEND_PACKAGE) +def sync_geofence_with_guardian(layer, perms, user=None, group=None): + """ + Sync Guardian permissions to GeoFence. + """ + # Create new rule-set + gf_services = {} + gf_services["*"] = 'download_resourcebase' in perms and \ + ('view_resourcebase' in perms or 'change_layer_style' in perms) + gf_services["WMS"] = 'view_resourcebase' in perms or 'change_layer_style' in perms + gf_services["GWC"] = 'view_resourcebase' in perms or 'change_layer_style' in perms + gf_services["WFS"] = ('download_resourcebase' in perms or 'change_layer_data' in perms) \ + and layer.is_vector() + gf_services["WCS"] = ('download_resourcebase' in perms or 'change_layer_data' in perms) \ + and not layer.is_vector() + gf_services["WPS"] = 'download_resourcebase' in perms or 'change_layer_data' in perms + + _user = None + if user: + _user = user if isinstance(user, basestring) else user.username + _group = None + if group: + _group = group if isinstance(group, basestring) else group.name + for service, allowed in gf_services.iteritems(): + if allowed: + if _user: + logger.debug("Adding 'user' to geofence the rule: %s %s %s" % (layer, service, _user)) + _update_geofence_rule(layer.name, layer.workspace, service, user=_user) + elif not _group: + logger.debug("Adding to geofence the rule: %s %s *" % (layer, service)) + _update_geofence_rule(layer.name, layer.workspace, service) + + if _group: + logger.debug("Adding 'group' to geofence the rule: %s %s %s" % (layer, service, _group)) + _update_geofence_rule(layer.name, layer.workspace, service, group=_group) + if not getattr(settings, 'DELAYED_SECURITY_SIGNALS', False): + set_geofence_invalidate_cache() + else: + layer.set_dirty_state() + + +def set_owner_permissions(resource): + """assign all admin permissions to the owner""" + if resource.polymorphic_ctype: + # Set the GeoFence Owner Rule + if resource.polymorphic_ctype.name == 'layer': + for perm in models.LAYER_ADMIN_PERMISSIONS: + assign_perm(perm, resource.owner, resource.layer) + for perm in models.ADMIN_PERMISSIONS: + assign_perm(perm, resource.owner, resource.get_self_resource()) + + +def remove_object_permissions(instance): + """Remove object permissions on given resource. + + If is a layer removes the layer specific permissions then the + resourcebase permissions + + """ + + from guardian.models import UserObjectPermission, GroupObjectPermission + resource = instance.get_self_resource() + if hasattr(resource, "layer"): + try: + UserObjectPermission.objects.filter( + content_type=ContentType.objects.get_for_model(resource.layer), + object_pk=instance.id + ).delete() + GroupObjectPermission.objects.filter( + content_type=ContentType.objects.get_for_model(resource.layer), + object_pk=instance.id + ).delete() + if settings.OGC_SERVER['default']['GEOFENCE_SECURITY_ENABLED']: + purge_geofence_layer_rules(resource) + except (ObjectDoesNotExist, RuntimeError): + pass # This layer is not manageable by geofence + except BaseException: + tb = traceback.format_exc() + logger.debug(tb) + finally: + if not getattr(settings, 'DELAYED_SECURITY_SIGNALS', False): + set_geofence_invalidate_cache() + else: + resource.set_dirty_state() + + UserObjectPermission.objects.filter(content_type=ContentType.objects.get_for_model(resource), + object_pk=instance.id).delete() + GroupObjectPermission.objects.filter(content_type=ContentType.objects.get_for_model(resource), + object_pk=instance.id).delete() + + +# # Logic to login a user automatically when it has successfully +# # activated an account: +# def autologin(sender, **kwargs): +# user = kwargs['user'] +# request = kwargs['request'] +# # Manually setting the default user backed to avoid the +# # 'User' object has no attribute 'backend' error +# user.backend = 'django.contrib.auth.backends.ModelBackend' +# # This login function does not need password. +# login(request, user) +# +# # FIXME(Ariel): Replace this signal with the one from django-user-accounts +# # user_activated.connect(autologin) + + +def _get_layer_workspace(layer): + """Get the workspace where the input layer belongs""" + workspace = layer.workspace + if not workspace: + default_workspace = getattr(settings, "DEFAULT_WORKSPACE", "geonode") + try: + if layer.remote_service.method == CASCADED: + workspace = getattr( + settings, "CASCADE_WORKSPACE", default_workspace) + else: + raise RuntimeError("Layer is not cascaded") + except AttributeError: # layer does not have a service + workspace = default_workspace + return workspace + + +def _get_geofence_payload(layer, workspace, access, user=None, group=None, + service=None): + highest_priority = get_highest_priority() + root_el = etree.Element("Rule") + username_el = etree.SubElement(root_el, "userName") + if user is not None: + username_el.text = user + else: + username_el.text = '' + priority_el = etree.SubElement(root_el, "priority") + priority_el.text = str(highest_priority if highest_priority >= 0 else 0) + if group is not None: + role_el = etree.SubElement(root_el, "roleName") + role_el.text = "ROLE_{}".format(group.upper()) + workspace_el = etree.SubElement(root_el, "workspace") + workspace_el.text = workspace + layer_el = etree.SubElement(root_el, "layer") + layer_el.text = layer + access_el = etree.SubElement(root_el, "access") + access_el.text = access + if service is not None and service is not "*": + service_el = etree.SubElement(root_el, "service") + service_el.text = service + return etree.tostring(root_el) + + +def _update_geofence_rule(layer, workspace, service, user=None, group=None): + payload = _get_geofence_payload( + layer=layer, + workspace=workspace, + access="ALLOW", + user=user, + group=group, + service=service + ) + logger.debug("request data: {}".format(payload)) + response = requests.post( + "{base_url}rest/geofence/rules".format( + base_url=settings.OGC_SERVER['default']['LOCATION']), + data=payload, + headers={ + 'Content-type': 'application/xml' + }, + auth=HTTPBasicAuth( + username=settings.OGC_SERVER['default']['USER'], + password=settings.OGC_SERVER['default']['PASSWORD'] + ) + ) + logger.debug("response status_code: {}".format(response.status_code)) + if response.status_code not in (200, 201): + msg = ("Could not ADD GeoServer User {!r} Rule for " + "Layer {!r}: '{!r}'".format(user, layer, response.text)) + if 'Duplicate Rule' in response.text: + logger.warning(msg) + else: + raise RuntimeError(msg) diff --git a/geonode/settings.py b/geonode/settings.py index 043ce2ff728..e13606c95b8 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -1419,7 +1419,7 @@ CELERY_ENABLE_UTC = True CELERY_TIMEZONE = TIME_ZONE CELERY_BEAT_SCHEDULE = { - 'send-summary-every-hour': { + 'delayed-security-sync-task': { 'task': 'geonode.security.tasks.synch_guardian', 'schedule': timedelta(seconds=60), }