Skip to content

Commit

Permalink
Merge pull request #7885 from darashevcstbg/5772-add-activity-streams…
Browse files Browse the repository at this point in the history
…-for-private-datasets

5772 add activity streams for private datasets
  • Loading branch information
wardi committed Nov 6, 2023
2 parents 8726efb + fb29ffe commit c8fd490
Show file tree
Hide file tree
Showing 24 changed files with 926 additions and 142 deletions.
5 changes: 5 additions & 0 deletions changes/5772.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enabled saving of activities on private datasets.

Added `permission_labels` column to `Activity` table.

Added filtering of dataset activities based on user permission labels.
6 changes: 0 additions & 6 deletions ckan/lib/create_test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,12 +471,6 @@ def create(cls, auth_profile: str = "",
])
cls.user_refs.extend([u'tester', u'joeadmin', u'annafan', u'russianfan', u'testsysadmin'])

# Create activities for packages
for item in [pkg1, pkg2]:
from ckanext.activity.model import Activity
activity = Activity.activity_stream_item(item, 'new', 'not logged in')
model.Session.add(activity)

model.repo.commit_and_remove()

# method used in DGU and all good tests elsewhere
Expand Down
11 changes: 6 additions & 5 deletions ckan/logic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,11 +347,12 @@ def check_access(action: str,
context['auth_user_obj'] = None

context = _prepopulate_context(context)
if not context.get('ignore_auth'):
if not context.get('__auth_user_obj_checked'):
if context["user"] and not context["auth_user_obj"]:
context['auth_user_obj'] = model.User.get(context['user'])
context['__auth_user_obj_checked'] = True

if not context.get('__auth_user_obj_checked'):
if context["user"] and not context["auth_user_obj"]:
context['auth_user_obj'] = model.User.get(context['user'])
context['__auth_user_obj_checked'] = True

try:
logic_authorization = authz.is_authorized(action, context, data_dict)
if not logic_authorization['success']:
Expand Down
4 changes: 1 addition & 3 deletions ckan/tests/controllers/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,6 @@ def test_create_user_as_sysadmin(self, app):
)
# assert "/user/activity" in response.headers["location"]

@pytest.mark.ckan_config("ckan.plugins", "activity")
@pytest.mark.usefixtures("with_plugins")
def test_registered_user_login(self, app):
"""
Registered user can submit valid login details at /user/login and
Expand All @@ -191,7 +189,7 @@ def test_registered_user_login(self, app):
},
)
# the response is the user dashboard, right?
assert '<a href="/dashboard/">Dashboard</a>' in response
assert '<a href="/dashboard/datasets">Dashboard</a>' in response
assert (
'<span class="username">{0}</span>'.format(user["fullname"])
in response
Expand Down
12 changes: 0 additions & 12 deletions ckan/tests/logic/test_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,6 @@ def test_check_access_auth_user_obj_is_set():
assert context["auth_user_obj"].name == user["name"]


@pytest.mark.usefixtures("non_clean_db")
def test_check_access_auth_user_obj_is_not_set_when_ignoring_auth():
user = factories.User()
context = {"user": user["name"], "ignore_auth": True}

result = logic.check_access("package_create", context)

assert result
assert "__auth_user_obj_checked" not in context
assert context["auth_user_obj"] is None


@mock.patch("ckan.authz.is_authorized")
def test_user_inside_context_of_check_access(is_authorized: mock.Mock):
logic.check_access("package_create", {})
Expand Down
39 changes: 34 additions & 5 deletions ckanext/activity/logic/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,28 @@
from typing import Any, Optional

import ckan.plugins.toolkit as tk

from ckan import authz
from ckan.logic import validate
from ckan.types import Context, DataDict, ActionResult
import ckanext.activity.email_notifications as email_notifications
from ckan.lib.plugins import get_permission_labels

from . import schema
from ..model import activity as model_activity, activity_dict_save

log = logging.getLogger(__name__)


def _get_user_permission_labels(
context: Context
):
if not authz.is_sysadmin(context.get('user')):
return get_permission_labels().get_user_dataset_labels(
context['auth_user_obj'])
else:
return None


def send_email_notifications(
context: Context, data_dict: DataDict
) -> ActionResult.SendEmailNotifications:
Expand Down Expand Up @@ -168,6 +179,7 @@ def user_activity_list(
offset=offset,
after=after,
before=before,
user_permission_labels=_get_user_permission_labels(context)
)

return model_activity.activity_list_dictize(activity_objects, context)
Expand Down Expand Up @@ -254,6 +266,7 @@ def package_activity_list(
include_hidden_activity=include_hidden_activity,
activity_types=activity_types,
exclude_activity_types=exclude_activity_types,
user_permission_labels=_get_user_permission_labels(context)
)

return model_activity.activity_list_dictize(activity_objects, context)
Expand Down Expand Up @@ -315,7 +328,8 @@ def group_activity_list(
after=after,
before=before,
include_hidden_activity=include_hidden_activity,
activity_types=activity_types
activity_types=activity_types,
user_permission_labels=_get_user_permission_labels(context)
)

return model_activity.activity_list_dictize(activity_objects, context)
Expand Down Expand Up @@ -374,7 +388,8 @@ def organization_activity_list(
after=after,
before=before,
include_hidden_activity=include_hidden_activity,
activity_types=activity_types
activity_types=activity_types,
user_permission_labels=_get_user_permission_labels(context)
)

return model_activity.activity_list_dictize(activity_objects, context)
Expand All @@ -401,11 +416,20 @@ def recently_changed_packages_activity_list(
"""
# FIXME: Filter out activities whose subject or object the user is not
# authorized to read.

tk.check_access(
"recently_changed_packages_activity_list",
context,
data_dict
)

offset = data_dict.get("offset", 0)
limit = data_dict["limit"] # defaulted, limited & made an int by schema

activity_objects = model_activity.recently_changed_packages_activity_list(
limit=limit, offset=offset
limit=limit,
offset=offset,
user_permission_labels=_get_user_permission_labels(context)
)

return model_activity.activity_list_dictize(activity_objects, context)
Expand Down Expand Up @@ -451,7 +475,12 @@ def dashboard_activity_list(
# FIXME: Filter out activities whose subject or object the user is not
# authorized to read.
activity_objects = model_activity.dashboard_activity_list(
user_id, limit=limit, offset=offset, before=before, after=after
user_id,
limit=limit,
offset=offset,
before=before,
after=after,
user_permission_labels=_get_user_permission_labels(context)
)

activity_dicts = model_activity.activity_list_dictize(
Expand Down
14 changes: 14 additions & 0 deletions ckanext/activity/logic/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import ckan.authz as authz
import ckan.plugins.toolkit as tk
from ckan.types import Context, DataDict, AuthResult
from ckan.lib.plugins import get_permission_labels

from ..model import Activity

Expand Down Expand Up @@ -146,6 +147,19 @@ def activity_show(context: Context, data_dict: DataDict) -> AuthResult:
# activity
if "package" in activity.activity_type:
object_type = "package"

# Check permission labels
user = context.get('user')
if not authz.is_sysadmin(user):
user_permission_labels = get_permission_labels(
).get_user_dataset_labels(context['auth_user_obj'])

if not any(permission in activity.permission_labels
for permission in user_permission_labels):
return {
'success': False,
'msg': tk._("Unauthorized to view activity data")
}
else:
return {"success": False, "msg": "object_type not recognized"}
return activity_list(
Expand Down
1 change: 1 addition & 0 deletions ckanext/activity/migration/activity/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
74 changes: 74 additions & 0 deletions ckanext/activity/migration/activity/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# A generic, single database configuration.

[alembic]
# path to migration scripts
script_location = %(here)s

# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =

# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; this defaults
# to /workspace/ckanext/activity/migration/activity/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat /workspace/ckanext/activity/migration/activity/versions

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

sqlalchemy.url = driver://user:pass@localhost/dbname


# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
81 changes: 81 additions & 0 deletions ckanext/activity/migration/activity/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-

from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig

import os

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.

name = os.path.basename(os.path.dirname(__file__))


def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""

url = config.get_main_option(u"sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True,
version_table=u'{}_alembic_version'.format(name)
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix=u'sqlalchemy.',
poolclass=pool.NullPool)

with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
version_table=u'{}_alembic_version'.format(name)
)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
24 changes: 24 additions & 0 deletions ckanext/activity/migration/activity/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}


def upgrade():
${upgrades if upgrades else "pass"}


def downgrade():
${downgrades if downgrades else "pass"}

0 comments on commit c8fd490

Please sign in to comment.