mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-05-13 04:41:43 +00:00
Add Page Visibility
This commit is contained in:
parent
70cdbf6ebf
commit
6529014cdf
39 changed files with 2691 additions and 239 deletions
backend
src/baserow/contrib/builder
tests/baserow/contrib/builder
changelog/entries/unreleased/feature
web-frontend
modules
builder
applicationTypes.js
components
elements/components/forms
form
page/settings
settings
sidebar
locales
mixins
pageSettingsTypes.jspages
plugin.jsutils
core
assets/scss/components
store
test/unit/builder/utils
|
@ -156,7 +156,16 @@ class PublicPageSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Page
|
||||
fields = ("id", "name", "path", "path_params", "shared")
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
"path",
|
||||
"path_params",
|
||||
"shared",
|
||||
"visibility",
|
||||
"role_type",
|
||||
"roles",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"builder_id": {"read_only": True},
|
||||
|
@ -237,6 +246,10 @@ class PublicBuilderSerializer(serializers.ModelSerializer):
|
|||
"the favicon settings."
|
||||
)
|
||||
|
||||
login_page_id = serializers.IntegerField(
|
||||
help_text=Builder._meta.get_field("login_page").help_text
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Builder
|
||||
fields = (
|
||||
|
@ -247,6 +260,7 @@ class PublicBuilderSerializer(serializers.ModelSerializer):
|
|||
"theme",
|
||||
"user_sources",
|
||||
"favicon_file",
|
||||
"login_page_id",
|
||||
)
|
||||
|
||||
@extend_schema_field(PublicPageSerializer(many=True))
|
||||
|
|
|
@ -28,11 +28,25 @@ class PageSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Page
|
||||
fields = ("id", "name", "path", "path_params", "order", "builder_id", "shared")
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
"path",
|
||||
"path_params",
|
||||
"order",
|
||||
"builder_id",
|
||||
"shared",
|
||||
"visibility",
|
||||
"role_type",
|
||||
"roles",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"builder_id": {"read_only": True},
|
||||
"shared": {"read_only": True},
|
||||
"visibility": {"read_only": True},
|
||||
"role_type": {"read_only": True},
|
||||
"roles": {"read_only": True},
|
||||
"order": {"help_text": "Lowest first."},
|
||||
}
|
||||
|
||||
|
@ -50,8 +64,14 @@ class UpdatePageSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Page
|
||||
fields = ("name", "path", "path_params")
|
||||
extra_kwargs = {"name": {"required": False}, "path": {"required": False}}
|
||||
fields = ("name", "path", "path_params", "visibility", "role_type", "roles")
|
||||
extra_kwargs = {
|
||||
"name": {"required": False},
|
||||
"path": {"required": False},
|
||||
"visibility": {"required": False},
|
||||
"role_type": {"required": False},
|
||||
"roles": {"required": False},
|
||||
}
|
||||
|
||||
|
||||
class OrderPagesSerializer(serializers.Serializer):
|
||||
|
|
|
@ -2,6 +2,7 @@ from typing import List
|
|||
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
|
||||
from baserow.contrib.builder.api.pages.serializers import PageSerializer
|
||||
from baserow.contrib.builder.api.theme.serializers import (
|
||||
|
@ -10,6 +11,7 @@ from baserow.contrib.builder.api.theme.serializers import (
|
|||
)
|
||||
from baserow.contrib.builder.models import Builder
|
||||
from baserow.contrib.builder.operations import ListPagesBuilderOperationType
|
||||
from baserow.contrib.builder.pages.handler import PageHandler
|
||||
from baserow.core.handler import CoreHandler
|
||||
|
||||
|
||||
|
@ -67,3 +69,17 @@ class BuilderSerializer(serializers.ModelSerializer):
|
|||
@extend_schema_field(CombinedThemeConfigBlocksSerializer())
|
||||
def get_theme(self, instance):
|
||||
return serialize_builder_theme(instance)
|
||||
|
||||
def validate_login_page_id(self, value: int) -> int:
|
||||
"""Validate the Builder's login_page."""
|
||||
|
||||
# Although only possible via the API, setting the login_page to the
|
||||
# shared page shouldn't be allowed because the shared page isn't
|
||||
# a real page.
|
||||
if value and PageHandler().get_page(value).shared:
|
||||
raise DRFValidationError(
|
||||
detail="The login page cannot be a shared page.",
|
||||
code="invalid_login_page_id",
|
||||
)
|
||||
|
||||
return value
|
||||
|
|
|
@ -7,6 +7,8 @@ from django.db import transaction
|
|||
from django.db.transaction import Atomic
|
||||
from django.urls import include, path
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from baserow.contrib.builder.builder_beta_init_application import (
|
||||
BuilderApplicationTypeInitApplication,
|
||||
)
|
||||
|
@ -46,9 +48,10 @@ class BuilderApplicationType(ApplicationType):
|
|||
"pages",
|
||||
"theme",
|
||||
"favicon_file",
|
||||
"login_page_id",
|
||||
]
|
||||
allowed_fields = ["favicon_file"]
|
||||
request_serializer_field_names = ["favicon_file"]
|
||||
allowed_fields = ["favicon_file", "login_page_id"]
|
||||
request_serializer_field_names = ["favicon_file", "login_page_id"]
|
||||
serializer_mixins = [lazy_get_instance_serializer_class]
|
||||
|
||||
# Builder applications are imported second.
|
||||
|
@ -67,6 +70,9 @@ class BuilderApplicationType(ApplicationType):
|
|||
help_text="The favicon image file",
|
||||
validators=[image_file_validation],
|
||||
),
|
||||
"login_page_id": serializers.IntegerField(
|
||||
allow_null=True, required=False, default=None
|
||||
),
|
||||
}
|
||||
|
||||
def get_api_urls(self):
|
||||
|
@ -195,6 +201,7 @@ class BuilderApplicationType(ApplicationType):
|
|||
theme=serialized_theme,
|
||||
user_sources=serialized_user_sources,
|
||||
favicon_file=serialized_favicon_file,
|
||||
login_page=builder.login_page,
|
||||
**serialized_builder,
|
||||
)
|
||||
|
||||
|
@ -381,6 +388,11 @@ class BuilderApplicationType(ApplicationType):
|
|||
builder.favicon_file = favicon_file
|
||||
builder.save()
|
||||
|
||||
if login_page := serialized_values.pop("login_page", None):
|
||||
if login_page_id := id_mapping["builder_pages"].get(login_page.id, None):
|
||||
builder.login_page_id = login_page_id
|
||||
builder.save()
|
||||
|
||||
ThemeHandler().import_theme(builder, serialized_theme, id_mapping)
|
||||
|
||||
return builder
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db.models import Q, QuerySet
|
||||
|
||||
from baserow.contrib.builder.elements.operations import ListElementsPageOperationType
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.contrib.builder.workflow_actions.operations import (
|
||||
DispatchBuilderWorkflowActionOperationType,
|
||||
ListBuilderWorkflowActionsPageOperationType,
|
||||
|
@ -166,6 +168,27 @@ class ElementVisibilityPermissionManager(PermissionManagerType):
|
|||
|
||||
return queryset
|
||||
|
||||
def exclude_elements_with_page_visibility(
|
||||
self,
|
||||
queryset: QuerySet,
|
||||
actor: AbstractUser,
|
||||
) -> QuerySet:
|
||||
"""
|
||||
Update the queryset by excluding all Elements that the user isn't
|
||||
allowed to view, based on the Page visibility settings.
|
||||
"""
|
||||
|
||||
if not getattr(actor, "is_authenticated", False):
|
||||
return queryset.exclude(page__visibility=Page.VISIBILITY_TYPES.LOGGED_IN)
|
||||
|
||||
return queryset.exclude(
|
||||
page__role_type=Page.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
page__roles__contains=actor.role,
|
||||
).exclude(
|
||||
Q(page__role_type=Page.ROLE_TYPES.DISALLOW_ALL_EXCEPT)
|
||||
& ~Q(page__roles__contains=actor.role),
|
||||
)
|
||||
|
||||
def exclude_elements_with_visibility(
|
||||
self,
|
||||
queryset: QuerySet,
|
||||
|
@ -204,6 +227,7 @@ class ElementVisibilityPermissionManager(PermissionManagerType):
|
|||
"""Filters out invisible elements and their workflow actions."""
|
||||
|
||||
if operation_name == ListElementsPageOperationType.type:
|
||||
queryset = self.exclude_elements_with_page_visibility(queryset, actor)
|
||||
if getattr(actor, "is_authenticated", False):
|
||||
queryset = self.exclude_elements_with_visibility(
|
||||
queryset, Element.VISIBILITY_TYPES.NOT_LOGGED
|
||||
|
@ -221,6 +245,8 @@ class ElementVisibilityPermissionManager(PermissionManagerType):
|
|||
queryset, Element.VISIBILITY_TYPES.LOGGED_IN
|
||||
)
|
||||
elif operation_name == ListBuilderWorkflowActionsPageOperationType.type:
|
||||
queryset = self.exclude_elements_with_page_visibility(queryset, actor)
|
||||
|
||||
prefix = "element__"
|
||||
if getattr(actor, "is_authenticated", False):
|
||||
queryset = self.exclude_elements_with_visibility(
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
# Generated by Django 5.0.9 on 2024-11-26 11:36
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("builder", "0040_datetimepickerelement"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="builder",
|
||||
name="login_page",
|
||||
field=models.OneToOneField(
|
||||
help_text="The login page for this application. This is related to the visibility settings of builder pages.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="login_page",
|
||||
to="builder.page",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="page",
|
||||
name="role_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("allow_all", "Allow All"),
|
||||
("allow_all_except", "Allow All Except"),
|
||||
("disallow_all_except", "Disallow All Except"),
|
||||
],
|
||||
db_default="allow_all",
|
||||
db_index=True,
|
||||
default="allow_all",
|
||||
help_text="Role type is used in conjunction with roles to control access to this page.",
|
||||
max_length=19,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="page",
|
||||
name="roles",
|
||||
field=models.JSONField(
|
||||
db_default=[],
|
||||
default=list,
|
||||
help_text="List of user roles that are associated with this page. Used in conjunction with role_type.",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="page",
|
||||
name="visibility",
|
||||
field=models.CharField(
|
||||
choices=[("all", "All"), ("logged-in", "Logged In")],
|
||||
db_default="all",
|
||||
db_index=True,
|
||||
default="all",
|
||||
help_text="Controls the page's visibility. When set to 'logged-in', the builder's login_page must also be set.",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -32,6 +32,14 @@ class Builder(Application):
|
|||
related_name="builder_favicon_file",
|
||||
)
|
||||
|
||||
login_page = models.OneToOneField(
|
||||
Page,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text="The login page for this application. This is related to the visibility settings of builder pages.",
|
||||
related_name="login_page",
|
||||
null=True,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = self.pk is None
|
||||
super().save(*args, **kwargs)
|
||||
|
|
|
@ -434,6 +434,9 @@ class PageHandler:
|
|||
elements=serialized_elements,
|
||||
data_sources=serialized_data_sources,
|
||||
workflow_actions=serialized_workflow_actions,
|
||||
visibility=page.visibility,
|
||||
role_type=page.role_type,
|
||||
roles=page.roles,
|
||||
)
|
||||
|
||||
def _ops_count_for_import_page(
|
||||
|
@ -593,6 +596,9 @@ class PageHandler:
|
|||
path=serialized_page["path"],
|
||||
path_params=serialized_page["path_params"],
|
||||
shared=False,
|
||||
visibility=serialized_page["visibility"],
|
||||
role_type=serialized_page["role_type"],
|
||||
roles=serialized_page["roles"],
|
||||
)
|
||||
|
||||
id_mapping["builder_pages"][serialized_page["id"]] = page_instance.id
|
||||
|
|
|
@ -27,6 +27,15 @@ class Page(
|
|||
OrderableMixin,
|
||||
models.Model,
|
||||
):
|
||||
class VISIBILITY_TYPES(models.TextChoices):
|
||||
ALL = "all"
|
||||
LOGGED_IN = "logged-in"
|
||||
|
||||
class ROLE_TYPES(models.TextChoices):
|
||||
ALLOW_ALL = "allow_all"
|
||||
ALLOW_ALL_EXCEPT = "allow_all_except"
|
||||
DISALLOW_ALL_EXCEPT = "disallow_all_except"
|
||||
|
||||
builder = models.ForeignKey("builder.Builder", on_delete=models.CASCADE)
|
||||
order = models.PositiveIntegerField()
|
||||
name = models.CharField(max_length=255)
|
||||
|
@ -40,6 +49,29 @@ class Page(
|
|||
# directly. They are created on demand when a shared element is created.
|
||||
shared = models.BooleanField(default=False, db_default=False)
|
||||
|
||||
visibility = models.CharField(
|
||||
choices=VISIBILITY_TYPES.choices,
|
||||
max_length=20,
|
||||
db_index=True,
|
||||
default=VISIBILITY_TYPES.ALL,
|
||||
db_default=VISIBILITY_TYPES.ALL,
|
||||
help_text="Controls the page's visibility. When set to 'logged-in', the builder's login_page must also be set.",
|
||||
)
|
||||
|
||||
role_type = models.CharField(
|
||||
choices=ROLE_TYPES.choices,
|
||||
max_length=19,
|
||||
db_index=True,
|
||||
default=ROLE_TYPES.ALLOW_ALL,
|
||||
db_default=ROLE_TYPES.ALLOW_ALL,
|
||||
help_text="Role type is used in conjunction with roles to control access to this page.",
|
||||
)
|
||||
roles = models.JSONField(
|
||||
default=list,
|
||||
db_default=[],
|
||||
help_text="List of user roles that are associated with this page. Used in conjunction with role_type.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = (
|
||||
"-shared", # First page is the shared one if any.
|
||||
|
|
|
@ -119,7 +119,9 @@ class PageService:
|
|||
context=page,
|
||||
)
|
||||
|
||||
allowed_updates = extract_allowed(kwargs, ["name", "path", "path_params"])
|
||||
allowed_updates = extract_allowed(
|
||||
kwargs, ["name", "path", "path_params", "visibility", "role_type", "roles"]
|
||||
)
|
||||
|
||||
self.handler.update_page(page, **allowed_updates)
|
||||
|
||||
|
|
|
@ -56,6 +56,9 @@ class PageDict(TypedDict):
|
|||
elements: List[ElementDict]
|
||||
data_sources: List[DataSourceDict]
|
||||
workflow_actions: List[WorkflowAction]
|
||||
visibility: Optional[str]
|
||||
role_type: Optional[str]
|
||||
roles: Optional[List[str]]
|
||||
|
||||
|
||||
class BuilderDict(TypedDict):
|
||||
|
@ -68,3 +71,4 @@ class BuilderDict(TypedDict):
|
|||
user_sources: List[UserSourceDictSubClass]
|
||||
theme: object
|
||||
favicon_file: Optional[dict]
|
||||
login_page: Optional[PageDict]
|
||||
|
|
|
@ -4,6 +4,7 @@ import pytest
|
|||
from rest_framework.status import HTTP_200_OK
|
||||
|
||||
from baserow.contrib.builder.elements.models import Element
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.core.user_sources.registries import user_source_type_registry
|
||||
from baserow.core.user_sources.user_source_user import UserSourceUser
|
||||
|
||||
|
@ -13,6 +14,9 @@ def data_source_fixture(data_fixture):
|
|||
"""A fixture to help test the PublicDispatchDataSourceView view."""
|
||||
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
workspace = data_fixture.create_workspace(user=user)
|
||||
database = data_fixture.create_database_application(workspace=workspace)
|
||||
|
||||
table, fields, rows = data_fixture.build_table(
|
||||
user=user,
|
||||
columns=[
|
||||
|
@ -24,21 +28,29 @@ def data_source_fixture(data_fixture):
|
|||
["Banana", "Yellow"],
|
||||
["Cherry", "Purple"],
|
||||
],
|
||||
database=database,
|
||||
)
|
||||
builder = data_fixture.create_builder_application(user=user)
|
||||
builder = data_fixture.create_builder_application(user=user, workspace=workspace)
|
||||
builder_to = data_fixture.create_builder_application(workspace=None)
|
||||
data_fixture.create_builder_custom_domain(builder=builder, published_to=builder_to)
|
||||
public_page = data_fixture.create_builder_page(builder=builder_to)
|
||||
|
||||
integration = data_fixture.create_local_baserow_integration(
|
||||
user=user, application=builder
|
||||
)
|
||||
|
||||
page = data_fixture.create_builder_page(user=user, builder=builder)
|
||||
|
||||
return {
|
||||
"user": user,
|
||||
"token": token,
|
||||
"page": page,
|
||||
"public_page": public_page,
|
||||
"integration": integration,
|
||||
"table": table,
|
||||
"rows": rows,
|
||||
"fields": fields,
|
||||
"builder_to": builder_to,
|
||||
}
|
||||
|
||||
|
||||
|
@ -78,7 +90,9 @@ def data_source_element_roles_fixture(data_fixture):
|
|||
}
|
||||
|
||||
|
||||
def create_user_table_and_role(data_fixture, user, builder, user_role):
|
||||
def create_user_table_and_role(
|
||||
data_fixture, user, builder, user_role, integration=None
|
||||
):
|
||||
"""Helper to create a User table with a particular user role."""
|
||||
|
||||
# Create the user table for the user_source
|
||||
|
@ -96,9 +110,11 @@ def create_user_table_and_role(data_fixture, user, builder, user_role):
|
|||
)
|
||||
email_field, name_field, password_field, role_field = user_fields
|
||||
|
||||
integration = data_fixture.create_local_baserow_integration(
|
||||
user=user, application=builder
|
||||
)
|
||||
if integration is None:
|
||||
integration = data_fixture.create_local_baserow_integration(
|
||||
user=user, application=builder
|
||||
)
|
||||
|
||||
user_source = data_fixture.create_user_source(
|
||||
user_source_type_registry.get("local_baserow").model_class,
|
||||
application=builder,
|
||||
|
@ -532,3 +548,322 @@ def test_dispatch_data_sources_list_rows_with_elements_and_role(
|
|||
"results": expected_results,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dispatch_data_sources_page_visibility_all_returns_elements(
|
||||
api_client, data_fixture, data_source_fixture
|
||||
):
|
||||
"""
|
||||
Test the DispatchDataSourcesView endpoint when the Page visibility allows
|
||||
access to the user.
|
||||
|
||||
If the page's visibility is set to 'all' and the user is anonymous, the
|
||||
endpoint should return elements.
|
||||
"""
|
||||
|
||||
page = data_source_fixture["page"]
|
||||
page.visibility = Page.VISIBILITY_TYPES.ALL
|
||||
page.save()
|
||||
|
||||
data_source = data_fixture.create_builder_local_baserow_list_rows_data_source(
|
||||
user=data_source_fixture["user"],
|
||||
page=page,
|
||||
integration=data_source_fixture["integration"],
|
||||
table=data_source_fixture["table"],
|
||||
)
|
||||
|
||||
# Create an element containing a formula
|
||||
field = data_source_fixture["fields"][0]
|
||||
data_fixture.create_builder_heading_element(
|
||||
page=page,
|
||||
value=f"get('data_source.{data_source.id}.0.field_{field.id}')",
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"api:builder:domains:public_dispatch_all",
|
||||
kwargs={"page_id": page.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url, {}, format="json")
|
||||
|
||||
# Since Page visiblity is 'all', the response should contain the resolved
|
||||
# formula even if the user is not logged in.
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
str(data_source.id): {
|
||||
"has_next_page": False,
|
||||
"results": [
|
||||
{f"field_{field.id}": "Apple"},
|
||||
{f"field_{field.id}": "Banana"},
|
||||
{f"field_{field.id}": "Cherry"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"roles",
|
||||
[
|
||||
[],
|
||||
["foo_role"],
|
||||
],
|
||||
)
|
||||
def test_dispatch_data_sources_page_visibility_logged_in_allow_all_returns_elements(
|
||||
api_client, data_fixture, data_source_fixture, roles
|
||||
):
|
||||
"""
|
||||
Test the DispatchDataSourcesView endpoint when the Page visibility allows
|
||||
access to the user.
|
||||
|
||||
If the page's visibility is set to 'logged-in' and the role_type is set to
|
||||
'allow_all', the endpoint should return elements regardless of the user's
|
||||
current role.
|
||||
"""
|
||||
|
||||
page = data_source_fixture["public_page"]
|
||||
page.visibility = Page.VISIBILITY_TYPES.LOGGED_IN
|
||||
page.role_type = Page.ROLE_TYPES.ALLOW_ALL
|
||||
page.roles = roles
|
||||
page.save()
|
||||
|
||||
integration = data_source_fixture["integration"]
|
||||
user = data_source_fixture["user"]
|
||||
user_source, _ = create_user_table_and_role(
|
||||
data_fixture,
|
||||
user,
|
||||
data_source_fixture["builder_to"],
|
||||
"foo_role",
|
||||
integration=integration,
|
||||
)
|
||||
user_source_user = UserSourceUser(
|
||||
user_source, None, 1, "foo_username", "foo@bar.com"
|
||||
)
|
||||
user_token = user_source_user.get_refresh_token().access_token
|
||||
|
||||
data_source = data_fixture.create_builder_local_baserow_list_rows_data_source(
|
||||
user=data_source_fixture["user"],
|
||||
page=page,
|
||||
integration=data_source_fixture["integration"],
|
||||
table=data_source_fixture["table"],
|
||||
)
|
||||
|
||||
# Create an element containing a formula
|
||||
field = data_source_fixture["fields"][0]
|
||||
data_fixture.create_builder_heading_element(
|
||||
page=page,
|
||||
value=f"get('data_source.{data_source.id}.0.field_{field.id}')",
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"api:builder:domains:public_dispatch_all",
|
||||
kwargs={"page_id": page.id},
|
||||
)
|
||||
|
||||
response = api_client.post(
|
||||
url, {}, format="json", HTTP_AUTHORIZATION=f"JWT {user_token}"
|
||||
)
|
||||
|
||||
# Since the request was made with an anonymous user and the Page visiblity
|
||||
# is 'logged-in', the response should *not* contain any resolved formulas.
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
str(data_source.id): {
|
||||
"has_next_page": False,
|
||||
"results": [
|
||||
{f"field_{field.id}": "Apple"},
|
||||
{f"field_{field.id}": "Banana"},
|
||||
{f"field_{field.id}": "Cherry"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dispatch_data_sources_page_visibility_logged_in_returns_no_elements_for_anon(
|
||||
api_client, data_fixture, data_source_fixture
|
||||
):
|
||||
"""
|
||||
Test the DispatchDataSourcesView endpoint when the Page visibility denies
|
||||
access to the user.
|
||||
|
||||
If the page's visibility is set to 'logged-in' and the user is anonymous, the
|
||||
endpoint should return zero elements.
|
||||
"""
|
||||
|
||||
page = data_source_fixture["page"]
|
||||
page.visibility = Page.VISIBILITY_TYPES.LOGGED_IN
|
||||
page.save()
|
||||
|
||||
data_source = data_fixture.create_builder_local_baserow_list_rows_data_source(
|
||||
user=data_source_fixture["user"],
|
||||
page=page,
|
||||
integration=data_source_fixture["integration"],
|
||||
table=data_source_fixture["table"],
|
||||
)
|
||||
|
||||
# Create an element containing a formula
|
||||
data_fixture.create_builder_heading_element(
|
||||
page=page,
|
||||
value=f"get('data_source.{data_source.id}.0.field_{data_source_fixture['fields'][0].id}')",
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"api:builder:domains:public_dispatch_all",
|
||||
kwargs={"page_id": page.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url, {}, format="json")
|
||||
|
||||
# Since the request was made with an anonymous user and the Page visiblity
|
||||
# is 'logged-in', the response should *not* contain any resolved formulas.
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
str(data_source.id): {
|
||||
"has_next_page": False,
|
||||
"results": [{}] * 3,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"user_role,role_type,roles,is_allowed",
|
||||
[
|
||||
(
|
||||
"foo_role",
|
||||
Page.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
[],
|
||||
# Allowed because "foo_role" isn't excluded
|
||||
True,
|
||||
),
|
||||
(
|
||||
"foo_role",
|
||||
Page.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
["bar_role"],
|
||||
# Allowed because "foo_role" isn't excluded
|
||||
True,
|
||||
),
|
||||
(
|
||||
"",
|
||||
Page.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
["bar_role"],
|
||||
# Allowed because "" isn't excluded
|
||||
True,
|
||||
),
|
||||
(
|
||||
"foo_role",
|
||||
Page.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
["foo_role"],
|
||||
# Disallowed because "foo_role" is excluded
|
||||
False,
|
||||
),
|
||||
(
|
||||
"foo_role",
|
||||
Page.ROLE_TYPES.DISALLOW_ALL_EXCEPT,
|
||||
["foo_role"],
|
||||
# Allowed because "foo_role" isn't excluded
|
||||
True,
|
||||
),
|
||||
(
|
||||
"",
|
||||
Page.ROLE_TYPES.DISALLOW_ALL_EXCEPT,
|
||||
["foo_role"],
|
||||
# Disallowed because "" isn't excluded
|
||||
False,
|
||||
),
|
||||
(
|
||||
"foo_role",
|
||||
Page.ROLE_TYPES.DISALLOW_ALL_EXCEPT,
|
||||
[],
|
||||
# Disallowed because "foo_role" isn't excluded
|
||||
False,
|
||||
),
|
||||
(
|
||||
"foo_role",
|
||||
Page.ROLE_TYPES.DISALLOW_ALL_EXCEPT,
|
||||
["foo_role"],
|
||||
# Allowed because "foo_role" is excluded
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_dispatch_data_sources_page_visibility_logged_in_allow_all_except(
|
||||
api_client,
|
||||
data_fixture,
|
||||
data_source_fixture,
|
||||
user_role,
|
||||
role_type,
|
||||
roles,
|
||||
is_allowed,
|
||||
):
|
||||
"""
|
||||
Test the DispatchDataSourcesView endpoint when the Page visibility allows
|
||||
access to the user.
|
||||
|
||||
If the page's visibility is set to 'logged-in' and the role_type is set to
|
||||
'allow_all', the endpoint should return elements regardless of the user's
|
||||
current role.
|
||||
"""
|
||||
|
||||
page = data_source_fixture["public_page"]
|
||||
page.visibility = Page.VISIBILITY_TYPES.LOGGED_IN
|
||||
page.role_type = role_type
|
||||
page.roles = roles
|
||||
page.save()
|
||||
|
||||
integration = data_source_fixture["integration"]
|
||||
user = data_source_fixture["user"]
|
||||
user_source, _ = create_user_table_and_role(
|
||||
data_fixture,
|
||||
user,
|
||||
data_source_fixture["builder_to"],
|
||||
user_role,
|
||||
integration=integration,
|
||||
)
|
||||
user_source_user = UserSourceUser(
|
||||
user_source, None, 1, "foo_username", "foo@bar.com"
|
||||
)
|
||||
user_token = user_source_user.get_refresh_token().access_token
|
||||
|
||||
data_source = data_fixture.create_builder_local_baserow_list_rows_data_source(
|
||||
user=user,
|
||||
page=page,
|
||||
integration=integration,
|
||||
table=data_source_fixture["table"],
|
||||
)
|
||||
|
||||
# Create an element containing a formula
|
||||
field = data_source_fixture["fields"][0]
|
||||
data_fixture.create_builder_heading_element(
|
||||
page=page,
|
||||
value=f"get('data_source.{data_source.id}.0.field_{field.id}')",
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"api:builder:domains:public_dispatch_all",
|
||||
kwargs={"page_id": page.id},
|
||||
)
|
||||
|
||||
response = api_client.post(
|
||||
url, {}, format="json", HTTP_AUTHORIZATION=f"JWT {user_token}"
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
if is_allowed:
|
||||
expected_results = [
|
||||
{f"field_{field.id}": "Apple"},
|
||||
{f"field_{field.id}": "Banana"},
|
||||
{f"field_{field.id}": "Cherry"},
|
||||
]
|
||||
else:
|
||||
expected_results = [{}, {}, {}]
|
||||
|
||||
assert response.json() == {
|
||||
str(data_source.id): {
|
||||
"has_next_page": False,
|
||||
"results": expected_results,
|
||||
},
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,6 +7,7 @@ from rest_framework.status import HTTP_200_OK
|
|||
|
||||
from baserow.api.user_files.serializers import UserFileSerializer
|
||||
from baserow.contrib.builder.models import Builder
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.contrib.builder.theme.registries import theme_config_block_registry
|
||||
|
||||
|
||||
|
@ -170,6 +171,7 @@ def test_get_builder_application(api_client, data_fixture):
|
|||
"name": workspace.name,
|
||||
"generative_ai_models_enabled": {},
|
||||
},
|
||||
"login_page_id": None,
|
||||
"pages": [
|
||||
{
|
||||
"id": application.page_set.get(shared=True).id,
|
||||
|
@ -179,6 +181,9 @@ def test_get_builder_application(api_client, data_fixture):
|
|||
"path": "__shared__",
|
||||
"path_params": [],
|
||||
"shared": True,
|
||||
"visibility": Page.VISIBILITY_TYPES.ALL.value,
|
||||
"role_type": Page.ROLE_TYPES.ALLOW_ALL.value,
|
||||
"roles": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -225,6 +230,7 @@ def test_list_builder_applications(api_client, data_fixture):
|
|||
"name": workspace.name,
|
||||
"generative_ai_models_enabled": {},
|
||||
},
|
||||
"login_page_id": None,
|
||||
"pages": [
|
||||
{
|
||||
"id": application.page_set.get(shared=True).id,
|
||||
|
@ -234,6 +240,9 @@ def test_list_builder_applications(api_client, data_fixture):
|
|||
"path": "__shared__",
|
||||
"path_params": [],
|
||||
"shared": True,
|
||||
"visibility": Page.VISIBILITY_TYPES.ALL.value,
|
||||
"role_type": Page.ROLE_TYPES.ALLOW_ALL.value,
|
||||
"roles": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
"""Test the BuilderSerializer serializer."""
|
||||
|
||||
from django.shortcuts import reverse
|
||||
|
||||
import pytest
|
||||
|
||||
from baserow.contrib.builder.api.serializers import BuilderSerializer
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def builder_fixture(data_fixture):
|
||||
"""A fixture to help test the BuilderSerializer."""
|
||||
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
workspace = data_fixture.create_workspace(user=user)
|
||||
builder = data_fixture.create_builder_application(workspace=workspace)
|
||||
page = data_fixture.create_builder_page(user=user, builder=builder)
|
||||
|
||||
return {
|
||||
"builder": builder,
|
||||
"page": page,
|
||||
"user": user,
|
||||
"token": token,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_serializer_has_expected_fields(builder_fixture):
|
||||
"""Ensure the serializer returns the expected fields."""
|
||||
|
||||
serializer = BuilderSerializer(instance=builder_fixture["builder"])
|
||||
assert sorted(serializer.data.keys()) == [
|
||||
"id",
|
||||
"integrations",
|
||||
"name",
|
||||
"pages",
|
||||
"theme",
|
||||
"user_sources",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_validate_login_page_id_raises_error_if_shared_page(
|
||||
api_client, builder_fixture
|
||||
):
|
||||
"""Ensure that only non-shared pages can be used as the login_page."""
|
||||
|
||||
builder = builder_fixture["builder"]
|
||||
|
||||
# Set the builder's page to be the shared page
|
||||
shared_page = builder.page_set.get(shared=True)
|
||||
response = api_client.patch(
|
||||
reverse("api:applications:item", kwargs={"application_id": builder.id}),
|
||||
{"login_page_id": shared_page.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {builder_fixture['token']}",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"detail": {
|
||||
"login_page_id": [
|
||||
{
|
||||
"code": "invalid_login_page_id",
|
||||
"error": "The login page cannot be a shared page.",
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": "ERROR_REQUEST_BODY_VALIDATION",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_login_page_is_saved(api_client, builder_fixture):
|
||||
"""Ensure that a valid page can be set as the Builder's login_page."""
|
||||
|
||||
builder = builder_fixture["builder"]
|
||||
assert builder.login_page is None
|
||||
|
||||
page = builder_fixture["page"]
|
||||
response = api_client.patch(
|
||||
reverse("api:applications:item", kwargs={"application_id": builder.id}),
|
||||
{"login_page_id": page.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {builder_fixture['token']}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
builder.refresh_from_db()
|
||||
assert builder.login_page == page
|
|
@ -7,6 +7,7 @@ from baserow.contrib.builder.elements.operations import ListElementsPageOperatio
|
|||
from baserow.contrib.builder.elements.permission_manager import (
|
||||
ElementVisibilityPermissionManager,
|
||||
)
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.contrib.builder.workflow_actions.models import BuilderWorkflowAction
|
||||
from baserow.contrib.builder.workflow_actions.operations import (
|
||||
DispatchBuilderWorkflowActionOperationType,
|
||||
|
@ -590,25 +591,25 @@ def test_queryset_only_includes_elements_allowed_by_role(
|
|||
)
|
||||
|
||||
# Create a workflow action connected to the element that requires the role
|
||||
workflow_action_logged_in = (
|
||||
data_fixture.create_local_baserow_create_row_workflow_action(
|
||||
page=public_page, element=element
|
||||
)
|
||||
data_fixture.create_local_baserow_create_row_workflow_action(
|
||||
page=public_page, element=element
|
||||
)
|
||||
|
||||
perm_manager = ElementVisibilityPermissionManager()
|
||||
|
||||
for operation_type in [
|
||||
ListBuilderWorkflowActionsPageOperationType.type,
|
||||
elements = perm_manager.filter_queryset(
|
||||
public_user_source_user,
|
||||
ListElementsPageOperationType.type,
|
||||
]:
|
||||
elements = perm_manager.filter_queryset(
|
||||
public_user_source_user,
|
||||
ListElementsPageOperationType.type,
|
||||
Element.objects.all(),
|
||||
)
|
||||
Element.objects.all(),
|
||||
)
|
||||
assert len(elements) == element_count
|
||||
|
||||
assert len(elements) == element_count
|
||||
actions = perm_manager.filter_queryset(
|
||||
public_user_source_user,
|
||||
ListBuilderWorkflowActionsPageOperationType.type,
|
||||
BuilderWorkflowAction.objects.all(),
|
||||
)
|
||||
assert len(actions) == element_count
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -970,3 +971,167 @@ def test_auth_user_can_view_element_returns_true(
|
|||
result = perm_manager.auth_user_can_view_element(user_source_user, element)
|
||||
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"page_visibility,page_role_type,page_roles,element_visibility,element_role_type,element_roles,user_role,expected_count",
|
||||
[
|
||||
# Both Page and Element visibility is permissive, so the Element
|
||||
# is returned.
|
||||
(
|
||||
Page.VISIBILITY_TYPES.ALL,
|
||||
Page.ROLE_TYPES.ALLOW_ALL,
|
||||
[],
|
||||
Element.VISIBILITY_TYPES.ALL,
|
||||
Element.ROLE_TYPES.ALLOW_ALL,
|
||||
[],
|
||||
"",
|
||||
1,
|
||||
),
|
||||
(
|
||||
Page.VISIBILITY_TYPES.ALL,
|
||||
Page.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
[],
|
||||
Element.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Element.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
[],
|
||||
"foo_role",
|
||||
1,
|
||||
),
|
||||
# Page allows visibility but Element does not, thus the Element
|
||||
# shouldn't be returned.
|
||||
(
|
||||
Page.VISIBILITY_TYPES.ALL,
|
||||
Page.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
[],
|
||||
Element.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Element.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
["foo_role"],
|
||||
"foo_role",
|
||||
0,
|
||||
),
|
||||
(
|
||||
Page.VISIBILITY_TYPES.ALL,
|
||||
Page.ROLE_TYPES.DISALLOW_ALL_EXCEPT,
|
||||
["foo_role"],
|
||||
Element.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Element.ROLE_TYPES.DISALLOW_ALL_EXCEPT,
|
||||
[],
|
||||
"foo_role",
|
||||
0,
|
||||
),
|
||||
# Page disallows visibility due to role, so despite the Element allowing
|
||||
# access, it shouldn't be returned.
|
||||
(
|
||||
Page.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Page.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
["foo_role"],
|
||||
Element.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Element.ROLE_TYPES.ALLOW_ALL,
|
||||
[],
|
||||
"foo_role",
|
||||
0,
|
||||
),
|
||||
(
|
||||
Page.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Page.ROLE_TYPES.DISALLOW_ALL_EXCEPT,
|
||||
[],
|
||||
Element.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Element.ROLE_TYPES.ALLOW_ALL,
|
||||
[],
|
||||
"foo_role",
|
||||
0,
|
||||
),
|
||||
# Page allows visibility and the Element role matches the user role, so
|
||||
# the Element should be returned.
|
||||
(
|
||||
Page.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Page.ROLE_TYPES.DISALLOW_ALL_EXCEPT,
|
||||
["foo_role"],
|
||||
Element.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Element.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
[],
|
||||
"foo_role",
|
||||
1,
|
||||
),
|
||||
(
|
||||
Page.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Page.ROLE_TYPES.ALLOW_ALL_EXCEPT,
|
||||
[],
|
||||
Element.VISIBILITY_TYPES.LOGGED_IN,
|
||||
Element.ROLE_TYPES.DISALLOW_ALL_EXCEPT,
|
||||
["foo_role"],
|
||||
"foo_role",
|
||||
1,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_page_visibility_applied_to_workflow_actions_queryset(
|
||||
ab_builder_user_page,
|
||||
data_fixture,
|
||||
page_visibility,
|
||||
page_role_type,
|
||||
page_roles,
|
||||
element_visibility,
|
||||
element_role_type,
|
||||
element_roles,
|
||||
user_role,
|
||||
expected_count,
|
||||
):
|
||||
"""
|
||||
Test the ElementVisibilityPermissionManager. Ensure that both Elements
|
||||
and Workflow Actions are filtered based on the Page visibility settings.
|
||||
"""
|
||||
|
||||
public_user_source, _, public_page = ab_builder_user_page
|
||||
|
||||
builder = public_page.builder
|
||||
|
||||
# Set the login page
|
||||
login_page = data_fixture.create_builder_page(builder=builder)
|
||||
builder.login_page = login_page
|
||||
|
||||
# Ensure the page is hidden
|
||||
public_page.visibility = page_visibility
|
||||
public_page.role_type = page_role_type
|
||||
public_page.roles = page_roles
|
||||
public_page.save()
|
||||
|
||||
public_user_source_user = UserSourceUser(
|
||||
public_user_source,
|
||||
None,
|
||||
1,
|
||||
"foo_username",
|
||||
"foo@bar.com",
|
||||
user_role,
|
||||
)
|
||||
|
||||
# Create an element
|
||||
element = data_fixture.create_builder_button_element(
|
||||
page=public_page,
|
||||
visibility=element_visibility,
|
||||
roles=element_roles,
|
||||
role_type=element_role_type,
|
||||
)
|
||||
|
||||
# Create a workflow action connected to the element that requires the role
|
||||
data_fixture.create_local_baserow_create_row_workflow_action(
|
||||
page=public_page, element=element
|
||||
)
|
||||
|
||||
perm_manager = ElementVisibilityPermissionManager()
|
||||
|
||||
elements = perm_manager.filter_queryset(
|
||||
public_user_source_user,
|
||||
ListElementsPageOperationType.type,
|
||||
Element.objects.all(),
|
||||
)
|
||||
assert len(elements) == expected_count
|
||||
|
||||
actions = perm_manager.filter_queryset(
|
||||
public_user_source_user,
|
||||
ListBuilderWorkflowActionsPageOperationType.type,
|
||||
BuilderWorkflowAction.objects.all(),
|
||||
)
|
||||
assert len(actions) == expected_count
|
||||
|
|
|
@ -11,6 +11,7 @@ from baserow.contrib.builder.elements.models import (
|
|||
LinkElement,
|
||||
)
|
||||
from baserow.contrib.builder.pages.handler import PageHandler
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.contrib.builder.workflow_actions.models import NotificationWorkflowAction
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.core.utils import MirrorDict
|
||||
|
@ -43,6 +44,9 @@ def test_repeat_element_import_child_with_formula_with_current_record(data_fixtu
|
|||
"order": 8,
|
||||
"path": "/page",
|
||||
"path_params": [],
|
||||
"visibility": Page.VISIBILITY_TYPES.ALL.value,
|
||||
"role_type": Page.ROLE_TYPES.ALLOW_ALL.value,
|
||||
"roles": [],
|
||||
"elements": [
|
||||
{
|
||||
"id": 23,
|
||||
|
|
|
@ -194,6 +194,9 @@ def test_builder_application_export(data_fixture):
|
|||
"path": shared_page.path,
|
||||
"path_params": shared_page.path_params,
|
||||
"shared": True,
|
||||
"visibility": Page.VISIBILITY_TYPES.ALL.value,
|
||||
"role_type": Page.ROLE_TYPES.ALLOW_ALL.value,
|
||||
"roles": [],
|
||||
"workflow_actions": [],
|
||||
"data_sources": [
|
||||
{
|
||||
|
@ -222,6 +225,9 @@ def test_builder_application_export(data_fixture):
|
|||
"path": page1.path,
|
||||
"path_params": page1.path_params,
|
||||
"shared": False,
|
||||
"visibility": Page.VISIBILITY_TYPES.ALL.value,
|
||||
"role_type": Page.ROLE_TYPES.ALLOW_ALL.value,
|
||||
"roles": [],
|
||||
"workflow_actions": [
|
||||
{
|
||||
"id": workflow_action_1.id,
|
||||
|
@ -398,6 +404,9 @@ def test_builder_application_export(data_fixture):
|
|||
"order": page2.order,
|
||||
"path": page2.path,
|
||||
"path_params": page2.path_params,
|
||||
"visibility": Page.VISIBILITY_TYPES.ALL.value,
|
||||
"role_type": Page.ROLE_TYPES.ALLOW_ALL.value,
|
||||
"roles": [],
|
||||
"shared": False,
|
||||
"workflow_actions": [],
|
||||
"data_sources": [
|
||||
|
@ -661,6 +670,7 @@ def test_builder_application_export(data_fixture):
|
|||
"order": builder.order,
|
||||
"type": "builder",
|
||||
"favicon_file": None,
|
||||
"login_page": None,
|
||||
}
|
||||
|
||||
assert serialized == reference
|
||||
|
@ -674,6 +684,9 @@ IMPORT_REFERENCE = {
|
|||
"order": 1,
|
||||
"path": "/test",
|
||||
"path_params": {},
|
||||
"visibility": Page.VISIBILITY_TYPES.ALL.value,
|
||||
"role_type": Page.ROLE_TYPES.ALLOW_ALL.value,
|
||||
"roles": [],
|
||||
"workflow_actions": [
|
||||
{
|
||||
"id": 123,
|
||||
|
@ -855,6 +868,9 @@ IMPORT_REFERENCE = {
|
|||
"path_params": {},
|
||||
"workflow_actions": [],
|
||||
"shared": False,
|
||||
"visibility": Page.VISIBILITY_TYPES.ALL.value,
|
||||
"role_type": Page.ROLE_TYPES.ALLOW_ALL.value,
|
||||
"roles": [],
|
||||
"elements": [
|
||||
{
|
||||
"id": 997,
|
||||
|
@ -1063,6 +1079,9 @@ IMPORT_REFERENCE_COMPLEX = {
|
|||
"path": "/test2",
|
||||
"path_params": {},
|
||||
"workflow_actions": [],
|
||||
"visibility": Page.VISIBILITY_TYPES.ALL.value,
|
||||
"role_type": Page.ROLE_TYPES.ALLOW_ALL.value,
|
||||
"roles": [],
|
||||
"elements": [
|
||||
{
|
||||
"id": 997,
|
||||
|
@ -1222,6 +1241,48 @@ def test_builder_application_imports_favicon_file(data_fixture, tmpdir):
|
|||
assert builder.favicon_file == user_file
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_builder_application_does_not_import_login_page(data_fixture):
|
||||
"""
|
||||
Ensure the importer doesn't attempt to import the login_page if it
|
||||
doesn't exist in the serialized values.
|
||||
"""
|
||||
|
||||
user = data_fixture.create_user(email="test@baserow.io")
|
||||
workspace = data_fixture.create_workspace(user=user)
|
||||
|
||||
config = ImportExportConfig(include_permission_data=True)
|
||||
serialized_values = IMPORT_REFERENCE.copy()
|
||||
serialized_values.pop("login_page", None)
|
||||
|
||||
builder = BuilderApplicationType().import_serialized(
|
||||
workspace, serialized_values, config, {}
|
||||
)
|
||||
|
||||
assert builder.login_page is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_builder_application_imports_login_page(data_fixture):
|
||||
"""Ensure the login_page is imported and saved to the builder."""
|
||||
|
||||
user = data_fixture.create_user()
|
||||
workspace = data_fixture.create_workspace(user=user)
|
||||
builder = data_fixture.create_builder_application(user=user, workspace=workspace)
|
||||
page = data_fixture.create_builder_page(builder=builder, name="foo_login_page")
|
||||
builder.login_page = page
|
||||
builder.save()
|
||||
|
||||
config = ImportExportConfig(include_permission_data=True)
|
||||
serialized = BuilderApplicationType().export_serialized(builder, config)
|
||||
|
||||
new_builder = BuilderApplicationType().import_serialized(
|
||||
workspace, serialized, config, {}
|
||||
)
|
||||
|
||||
assert new_builder.login_page.name == "foo_login_page"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_builder_application_with_published_builder(data_fixture):
|
||||
builder = data_fixture.create_builder_application()
|
||||
|
|
|
@ -305,15 +305,19 @@ def test_get_element_property_names_returns_property_names(data_fixture):
|
|||
data_source_1.service.id,
|
||||
data_source_2.service.id,
|
||||
]
|
||||
assert sorted(list(result["external"][data_source_1.service.id])) == [
|
||||
# Since only the first two fields are used by elements in this page,
|
||||
# we expect to see _only_ those two fields.
|
||||
f"field_{fields[0].id}",
|
||||
f"field_{fields[1].id}",
|
||||
]
|
||||
assert sorted(list(result["external"][data_source_2.service.id])) == [
|
||||
f"field_{fields[2].id}",
|
||||
]
|
||||
assert sorted(list(result["external"][data_source_1.service.id])) == sorted(
|
||||
[
|
||||
# Since only the first two fields are used by elements in this page,
|
||||
# we expect to see _only_ those two fields.
|
||||
f"field_{fields[0].id}",
|
||||
f"field_{fields[1].id}",
|
||||
]
|
||||
)
|
||||
assert sorted(list(result["external"][data_source_2.service.id])) == sorted(
|
||||
[
|
||||
f"field_{fields[2].id}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "[Builder] Improve Builder security with Page level Visibility.",
|
||||
"issue_number": 2392,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-11-06"
|
||||
}
|
|
@ -77,9 +77,10 @@ export class BuilderApplicationType extends ApplicationType {
|
|||
|
||||
delete(application) {
|
||||
const { store, router } = this.app
|
||||
const pageSelected = store.getters['page/getAllPages'](application).some(
|
||||
(page) => page._.selected
|
||||
)
|
||||
const pageSelected = store.getters['page/getVisiblePages'](
|
||||
application
|
||||
).some((page) => page._.selected)
|
||||
|
||||
if (pageSelected) {
|
||||
router.push({ name: 'dashboard' })
|
||||
}
|
||||
|
@ -141,6 +142,14 @@ export class BuilderApplicationType extends ApplicationType {
|
|||
}
|
||||
|
||||
prepareForStoreUpdate(application, data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'pages')) {
|
||||
delete data.pages
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'theme')) {
|
||||
delete data.theme
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
|
|
|
@ -78,217 +78,24 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||
import UserSourceService from '@baserow/modules/core/services/userSource'
|
||||
import visibilityForm from '@baserow/modules/builder/mixins/visibilityForm'
|
||||
|
||||
import {
|
||||
DEFAULT_USER_ROLE_PREFIX,
|
||||
VISIBILITY_ALL,
|
||||
VISIBILITY_NOT_LOGGED,
|
||||
VISIBILITY_LOGGED_IN,
|
||||
ROLE_TYPE_ALLOW_ALL,
|
||||
ROLE_TYPE_ALLOW_EXCEPT,
|
||||
ROLE_TYPE_DISALLOW_EXCEPT,
|
||||
} from '@baserow/modules/builder/constants'
|
||||
|
||||
export default {
|
||||
name: 'VisibilityForm',
|
||||
mixins: [elementForm],
|
||||
mixins: [visibilityForm],
|
||||
data() {
|
||||
return {
|
||||
allRoles: [],
|
||||
values: {
|
||||
visibility: VISIBILITY_ALL,
|
||||
roles: [],
|
||||
role_type: ROLE_TYPE_ALLOW_ALL,
|
||||
},
|
||||
visibilityLoggedIn: VISIBILITY_LOGGED_IN,
|
||||
visibilityAll: VISIBILITY_ALL,
|
||||
visibilityNotLogged: VISIBILITY_NOT_LOGGED,
|
||||
loadingRoles: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
elementSelected: 'element/getSelected',
|
||||
}),
|
||||
allowAllRolesExceptSelected() {
|
||||
return this.values.role_type === ROLE_TYPE_ALLOW_EXCEPT
|
||||
},
|
||||
disallowAllRolesExceptSelected() {
|
||||
return this.values.role_type === ROLE_TYPE_DISALLOW_EXCEPT
|
||||
},
|
||||
roleTypeOptions() {
|
||||
return {
|
||||
[ROLE_TYPE_ALLOW_ALL]: this.$t('visibilityForm.roleTypeAllowAllRoles'),
|
||||
[ROLE_TYPE_DISALLOW_EXCEPT]: this.$t(
|
||||
'visibilityForm.roleTypeDisallowAllRolesExcept'
|
||||
),
|
||||
[ROLE_TYPE_ALLOW_EXCEPT]: this.$t(
|
||||
'visibilityForm.roleTypeAllowAllRolesExcept'
|
||||
),
|
||||
}
|
||||
},
|
||||
userSources() {
|
||||
return this.$store.getters['userSource/getUserSources'](this.builder)
|
||||
},
|
||||
/**
|
||||
* When the user changes the role_type, e.g. 'Allow roles...',
|
||||
* reset the element's roles.
|
||||
*/
|
||||
selectedRoleType: {
|
||||
get() {
|
||||
return this.values.role_type
|
||||
},
|
||||
set(newValue) {
|
||||
this.values.role_type = newValue
|
||||
|
||||
if (
|
||||
[ROLE_TYPE_ALLOW_EXCEPT, ROLE_TYPE_DISALLOW_EXCEPT].includes(newValue)
|
||||
) {
|
||||
this.values.roles = []
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* When the user changes the visibility, e.g. 'All visitors' radio button,
|
||||
* reset the element's roles.
|
||||
*/
|
||||
selectedVisibility: {
|
||||
get() {
|
||||
return this.values.visibility
|
||||
},
|
||||
set(newValue) {
|
||||
this.values.visibility = newValue
|
||||
this.resetElementRoles()
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* If the userSource changes, we want to ensure the roles for the element
|
||||
* are still valid.
|
||||
*/
|
||||
userSources: {
|
||||
handler() {
|
||||
this.fetchAndSyncUserRoles()
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.fetchAndSyncUserRoles()
|
||||
},
|
||||
methods: {
|
||||
checkRole(roleName) {
|
||||
if (this.values.roles.includes(roleName)) {
|
||||
this.values.roles = this.values.roles.filter(
|
||||
(role) => role !== roleName
|
||||
)
|
||||
} else {
|
||||
this.values.roles.push(roleName)
|
||||
}
|
||||
},
|
||||
|
||||
isChecked(roleName) {
|
||||
return this.values.roles.includes(roleName)
|
||||
},
|
||||
|
||||
async fetchAndSyncUserRoles() {
|
||||
await this.fetchUserRoles()
|
||||
this.syncUserRoles()
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch all valid user roles.
|
||||
*/
|
||||
async fetchUserRoles() {
|
||||
this.loadingRoles = true
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await UserSourceService(this.$client).fetchUserRoles(
|
||||
this.builder.id
|
||||
)
|
||||
} catch (error) {
|
||||
this.$store.dispatch('toast/error', {
|
||||
title: this.$t('visibilityForm.errorFetchingRolesTitle'),
|
||||
message: this.$t('visibilityForm.errorFetchingRolesMessage'),
|
||||
})
|
||||
return
|
||||
} finally {
|
||||
this.loadingRoles = false
|
||||
}
|
||||
|
||||
let noRole = false
|
||||
let allRoles = Array.from(
|
||||
new Set(
|
||||
result.data
|
||||
.map((userSourceData) => {
|
||||
return userSourceData.roles.filter((role) => {
|
||||
if (role === '') {
|
||||
noRole = true
|
||||
}
|
||||
return role !== ''
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
)
|
||||
).sort()
|
||||
|
||||
// If noRole exists, prefix to array
|
||||
if (noRole) {
|
||||
allRoles = ['', ...allRoles]
|
||||
}
|
||||
|
||||
this.allRoles = allRoles
|
||||
},
|
||||
|
||||
getRoleName(roleName) {
|
||||
if (roleName === '') {
|
||||
return this.$t('visibilityForm.noRole')
|
||||
} else if (roleName.startsWith(DEFAULT_USER_ROLE_PREFIX)) {
|
||||
const userSourceId = parseInt(
|
||||
roleName.split(DEFAULT_USER_ROLE_PREFIX)[1]
|
||||
)
|
||||
const userSource = this.$store.getters['userSource/getUserSourceById'](
|
||||
this.builder,
|
||||
userSourceId
|
||||
)
|
||||
if (userSource) {
|
||||
return this.$t('visibilityForm.rolesAllMembersOf', {
|
||||
name: userSource.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return roleName
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure that an element's roles never contains any old or otherwise
|
||||
* invalid roles. This is done by getting the latest valid roles from the
|
||||
* backend and comparing it to the element's roles.
|
||||
*/
|
||||
syncUserRoles() {
|
||||
const validRoles = this.allRoles.filter((role) => {
|
||||
return this.values.roles.includes(role)
|
||||
})
|
||||
|
||||
this.values.roles = validRoles
|
||||
},
|
||||
resetElementRoles() {
|
||||
this.values.roles = []
|
||||
this.values.role_type = ROLE_TYPE_ALLOW_ALL
|
||||
},
|
||||
|
||||
selectAllRoles() {
|
||||
this.values.roles = [...this.allRoles]
|
||||
},
|
||||
|
||||
deselectAllRoles() {
|
||||
this.values.roles = []
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<form @submit.prevent @keydown.enter.prevent>
|
||||
<FormGroup
|
||||
horizontal-narrow
|
||||
:label="$t('builderLoginPageForm.pageDropdownLabel')"
|
||||
required
|
||||
class="margin-top-4"
|
||||
:help-icon-tooltip="$t('builderLoginPageForm.pageDropdownDescription')"
|
||||
>
|
||||
<Dropdown
|
||||
v-model="values.login_page_id"
|
||||
:placeholder="$t('builderLoginPageForm.pageDropdownPlaceholder')"
|
||||
>
|
||||
<DropdownItem
|
||||
v-for="page in builderPages"
|
||||
:key="page.id"
|
||||
:name="page.name"
|
||||
:value="page.id"
|
||||
/>
|
||||
</Dropdown>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
|
||||
export default {
|
||||
name: 'BuilderLoginPageForm',
|
||||
mixins: [form],
|
||||
props: {
|
||||
builder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
values: { login_page_id: null },
|
||||
allowedValues: ['login_page_id'],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
builderPages() {
|
||||
return this.$store.getters['page/getVisiblePages'](this.builder)
|
||||
},
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<form @submit.prevent @keydown.enter.prevent>
|
||||
<FormGroup
|
||||
:label="$t('pageVisibilitySettings.description')"
|
||||
small-label
|
||||
class="margin-bottom-2"
|
||||
required
|
||||
>
|
||||
<div>
|
||||
<Alert v-if="showLoginPageAlert && showLogInPageWarning" type="warning">
|
||||
<slot name="title">{{
|
||||
$t('pageVisibilitySettingsTypes.logInPageWarningTitle')
|
||||
}}</slot>
|
||||
<!-- eslint-disable-next-line vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<p
|
||||
v-html="$t('pageVisibilitySettingsTypes.logInPagewarningMessage')"
|
||||
></p>
|
||||
</Alert>
|
||||
<Alert
|
||||
v-else-if="showLoginPageAlert && !showLogInPageWarning"
|
||||
type="info-primary"
|
||||
>
|
||||
<slot name="title">{{
|
||||
$t('pageVisibilitySettingsTypes.logInPageInfoTitle')
|
||||
}}</slot>
|
||||
<!-- eslint-disable-next-line vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<p
|
||||
v-html="
|
||||
$t('pageVisibilitySettingsTypes.logInPageInfoMessage', {
|
||||
logInPageName: loginPageName,
|
||||
})
|
||||
"
|
||||
></p>
|
||||
</Alert>
|
||||
</div>
|
||||
<div class="margin-top-1 visibility-form__visibility-all">
|
||||
<div>
|
||||
<Radio v-model="values.visibility" :value="visibilityAll">
|
||||
{{ $t('pageVisibilitySettings.allVisitors') }}
|
||||
</Radio>
|
||||
</div>
|
||||
|
||||
<div class="margin-left-2 visibility-form__visibility-logged-in">
|
||||
<Expandable
|
||||
:force-expanded="selectedVisibility === visibilityLoggedIn"
|
||||
>
|
||||
<template #header="{ toggle }">
|
||||
<Radio
|
||||
v-model="selectedVisibility"
|
||||
:value="visibilityLoggedIn"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ $t('pageVisibilitySettings.loggedInVisitors') }}
|
||||
</Radio>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<FormElement
|
||||
class="control visibility-form__expanded-form-element"
|
||||
>
|
||||
<Dropdown
|
||||
v-model="selectedRoleType"
|
||||
:placeholder="$t('visibilityForm.roleTypesHint')"
|
||||
>
|
||||
<DropdownItem
|
||||
v-for="(value, key) in roleTypeOptions"
|
||||
:key="key"
|
||||
:name="value"
|
||||
:value="key"
|
||||
>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
allowAllRolesExceptSelected ||
|
||||
disallowAllRolesExceptSelected
|
||||
"
|
||||
class="visibility-form__role-checkbox-container"
|
||||
>
|
||||
<template v-if="loadingRoles">
|
||||
<div class="loading margin-bottom-1"></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="roleName in allRoles"
|
||||
:key="roleName"
|
||||
class="visibility-form__role-checkbox-div"
|
||||
>
|
||||
<Checkbox
|
||||
:checked="isChecked(roleName)"
|
||||
@input="checkRole(roleName)"
|
||||
>
|
||||
{{ getRoleName(roleName) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div class="visibility-form__role-links">
|
||||
<a @click.prevent="selectAllRoles">
|
||||
{{ $t('visibilityForm.rolesSelectAll') }}
|
||||
</a>
|
||||
<a
|
||||
class="visibility-form__role-links-deselect-all"
|
||||
@click.prevent="deselectAllRoles"
|
||||
>
|
||||
{{ $t('visibilityForm.rolesDeselectAll') }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</FormElement>
|
||||
</template>
|
||||
</Expandable>
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { StoreItemLookupError } from '@baserow/modules/core/errors'
|
||||
import visibilityForm from '@baserow/modules/builder/mixins/visibilityForm'
|
||||
|
||||
import { VISIBILITY_LOGGED_IN } from '@baserow/modules/builder/constants'
|
||||
|
||||
export default {
|
||||
name: 'PageVisibilityForm',
|
||||
mixins: [visibilityForm],
|
||||
data() {
|
||||
return {
|
||||
values: {
|
||||
visibility: this.defaultValues.visibility,
|
||||
roles: this.defaultValues.roles,
|
||||
role_type: this.defaultValues.role_type,
|
||||
},
|
||||
allowedValues: ['visibility'],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showLoginPageAlert() {
|
||||
return this.selectedVisibility === VISIBILITY_LOGGED_IN
|
||||
},
|
||||
showLogInPageWarning() {
|
||||
return !this.loginPageName
|
||||
},
|
||||
/**
|
||||
* Return the login page's name or null if the page doesn't exist.
|
||||
*/
|
||||
loginPageName() {
|
||||
try {
|
||||
const loginPage = this.$store.getters['page/getById'](
|
||||
this.builder,
|
||||
this.builder.login_page_id
|
||||
)
|
||||
return loginPage.name
|
||||
} catch (e) {
|
||||
if (e instanceof StoreItemLookupError) {
|
||||
return null
|
||||
}
|
||||
throw e
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2 class="box__title">{{ $t('pageVisibilitySettings.title') }}</h2>
|
||||
|
||||
<div>
|
||||
<PageVisibilityForm
|
||||
:default-values="page"
|
||||
@values-changed="updatePageVisibility"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import PageVisibilityForm from '@baserow/modules/builder/components/page/settings/PageVisibilityForm'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'PageVisibilitySettings',
|
||||
components: { PageVisibilityForm },
|
||||
mixins: [error],
|
||||
inject: ['builder', 'page', 'workspace'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({ actionUpdatePage: 'page/update' }),
|
||||
async updatePageVisibility(values) {
|
||||
this.hideError()
|
||||
try {
|
||||
await this.actionUpdatePage({
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
values,
|
||||
})
|
||||
} catch (error) {
|
||||
this.handleError(error)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -7,17 +7,25 @@
|
|||
:default-values="builder"
|
||||
@values-changed="updateApplication($event)"
|
||||
/>
|
||||
|
||||
<BuilderLoginPageForm
|
||||
:default-values="builder"
|
||||
:builder="builder"
|
||||
@values-changed="updateApplication($event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import BuilderGeneralSettingsForm from '@baserow/modules/builder/components/form/BuilderGeneralSettingsForm'
|
||||
import BuilderLoginPageForm from '@baserow/modules/builder/components/form/BuilderLoginPageForm'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'GeneralSettings',
|
||||
components: { BuilderGeneralSettingsForm },
|
||||
components: { BuilderGeneralSettingsForm, BuilderLoginPageForm },
|
||||
mixins: [error],
|
||||
provide() {
|
||||
return {
|
||||
|
|
|
@ -10,8 +10,13 @@
|
|||
<Editable
|
||||
ref="rename"
|
||||
:value="page.name"
|
||||
class="side-bar-builder__link-text"
|
||||
@change="renamePage(builder, page, $event)"
|
||||
></Editable>
|
||||
<i
|
||||
v-if="page.visibility === visibilityLoggedIn"
|
||||
class="iconoir-eye-off"
|
||||
></i>
|
||||
</a>
|
||||
|
||||
<a
|
||||
|
@ -79,6 +84,7 @@
|
|||
<script>
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { VISIBILITY_LOGGED_IN } from '@baserow/modules/builder/constants'
|
||||
|
||||
export default {
|
||||
name: 'SidebarItemBuilder',
|
||||
|
@ -118,6 +124,9 @@ export default {
|
|||
)
|
||||
)
|
||||
},
|
||||
visibilityLoggedIn() {
|
||||
return VISIBILITY_LOGGED_IN
|
||||
},
|
||||
...mapGetters({ duplicateJob: 'page/getDuplicateJob' }),
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -338,11 +338,24 @@
|
|||
"pageSettingsTypes": {
|
||||
"pageName": "Page"
|
||||
},
|
||||
"pageVisibilitySettingsTypes": {
|
||||
"pageName": "Visibility",
|
||||
"logInPageWarningTitle": "The Login Page is not set",
|
||||
"logInPagewarningMessage": "When an anonymous user attempts to access this page, they will be redirected to a login page. Please set the Login Page in the Application's General settings.",
|
||||
"logInPageInfoTitle": "Anonymous users will be redirected",
|
||||
"logInPageInfoMessage": "Anonymous users attempting to access this page will be redirected to the {logInPageName} page."
|
||||
},
|
||||
"pageSettings": {
|
||||
"title": "Page",
|
||||
"pageUpdatedTitle": "Changed",
|
||||
"pageUpdatedDescription": "The page settings have been updated."
|
||||
},
|
||||
"pageVisibilitySettings": {
|
||||
"title": "Visibility",
|
||||
"allVisitors": "All visitors",
|
||||
"loggedInVisitors": "Logged-in visitors",
|
||||
"description": "Who can see this page"
|
||||
},
|
||||
"pageForm": {
|
||||
"defaultName": "Page",
|
||||
"nameTitle": "Name",
|
||||
|
@ -701,6 +714,11 @@
|
|||
"authTitle": "Authentication",
|
||||
"integrationFieldLabel": "Integration"
|
||||
},
|
||||
"builderLoginPageForm": {
|
||||
"pageDropdownLabel": "Login Page",
|
||||
"pageDropdownPlaceholder": "Select a page",
|
||||
"pageDropdownDescription": "Select the Login Page that non-logged in users should be redirected to when they attempt to access a restricted page."
|
||||
},
|
||||
"formContainerElement": {
|
||||
"submitDeactivatedText": "Complete all required fields to submit",
|
||||
"defaultSubmitButtonLabel": "Submit"
|
||||
|
|
210
web-frontend/modules/builder/mixins/visibilityForm.js
Normal file
210
web-frontend/modules/builder/mixins/visibilityForm.js
Normal file
|
@ -0,0 +1,210 @@
|
|||
import UserSourceService from '@baserow/modules/core/services/userSource'
|
||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||
|
||||
import {
|
||||
DEFAULT_USER_ROLE_PREFIX,
|
||||
ROLE_TYPE_ALLOW_ALL,
|
||||
ROLE_TYPE_ALLOW_EXCEPT,
|
||||
ROLE_TYPE_DISALLOW_EXCEPT,
|
||||
VISIBILITY_ALL,
|
||||
VISIBILITY_LOGGED_IN,
|
||||
VISIBILITY_NOT_LOGGED,
|
||||
} from '@baserow/modules/builder/constants'
|
||||
|
||||
export default {
|
||||
mixins: [elementForm],
|
||||
data() {
|
||||
return {
|
||||
allRoles: [],
|
||||
visibilityLoggedIn: VISIBILITY_LOGGED_IN,
|
||||
visibilityAll: VISIBILITY_ALL,
|
||||
visibilityNotLogged: VISIBILITY_NOT_LOGGED,
|
||||
loadingRoles: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
allowAllRolesExceptSelected() {
|
||||
return this.values.role_type === ROLE_TYPE_ALLOW_EXCEPT
|
||||
},
|
||||
|
||||
disallowAllRolesExceptSelected() {
|
||||
return this.values.role_type === ROLE_TYPE_DISALLOW_EXCEPT
|
||||
},
|
||||
|
||||
roleTypeOptions() {
|
||||
return {
|
||||
[ROLE_TYPE_ALLOW_ALL]: this.$t('visibilityForm.roleTypeAllowAllRoles'),
|
||||
[ROLE_TYPE_DISALLOW_EXCEPT]: this.$t(
|
||||
'visibilityForm.roleTypeDisallowAllRolesExcept'
|
||||
),
|
||||
[ROLE_TYPE_ALLOW_EXCEPT]: this.$t(
|
||||
'visibilityForm.roleTypeAllowAllRolesExcept'
|
||||
),
|
||||
}
|
||||
},
|
||||
|
||||
userSources() {
|
||||
return this.$store.getters['userSource/getUserSources'](this.builder)
|
||||
},
|
||||
|
||||
/**
|
||||
* When the user changes the role_type, e.g. 'Allow roles...',
|
||||
* reset the Page or Element roles.
|
||||
*/
|
||||
selectedRoleType: {
|
||||
get() {
|
||||
return this.values.role_type
|
||||
},
|
||||
set(newValue) {
|
||||
this.values.role_type = newValue
|
||||
|
||||
if (
|
||||
[ROLE_TYPE_ALLOW_EXCEPT, ROLE_TYPE_DISALLOW_EXCEPT].includes(newValue)
|
||||
) {
|
||||
this.values.roles = []
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* When the user changes the visibility, e.g. 'All visitors' radio button,
|
||||
* reset the Page or Element roles.
|
||||
*/
|
||||
selectedVisibility: {
|
||||
get() {
|
||||
return this.values.visibility
|
||||
},
|
||||
set(newValue) {
|
||||
this.values.visibility = newValue
|
||||
this.resetElementRoles()
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* If the userSource changes, we want to ensure the roles for the
|
||||
* Page/Element are still valid.
|
||||
*/
|
||||
userSources: {
|
||||
handler() {
|
||||
this.fetchAndSyncUserRoles()
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.fetchAndSyncUserRoles()
|
||||
},
|
||||
methods: {
|
||||
checkRole(roleName) {
|
||||
if (this.values.roles.includes(roleName)) {
|
||||
this.values.roles = this.values.roles.filter(
|
||||
(role) => role !== roleName
|
||||
)
|
||||
} else {
|
||||
this.values.roles.push(roleName)
|
||||
}
|
||||
},
|
||||
|
||||
isChecked(roleName) {
|
||||
return this.values.roles.includes(roleName)
|
||||
},
|
||||
|
||||
async fetchAndSyncUserRoles() {
|
||||
await this.fetchUserRoles()
|
||||
this.syncUserRoles()
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch all valid user roles.
|
||||
*/
|
||||
async fetchUserRoles() {
|
||||
this.loadingRoles = true
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await UserSourceService(this.$client).fetchUserRoles(
|
||||
this.builder.id
|
||||
)
|
||||
} catch (error) {
|
||||
this.$store.dispatch('toast/error', {
|
||||
title: this.$t('visibilityForm.errorFetchingRolesTitle'),
|
||||
message: this.$t('visibilityForm.errorFetchingRolesMessage'),
|
||||
})
|
||||
return
|
||||
} finally {
|
||||
this.loadingRoles = false
|
||||
}
|
||||
|
||||
let noRole = false
|
||||
let allRoles = Array.from(
|
||||
new Set(
|
||||
result.data
|
||||
.map((userSourceData) => {
|
||||
return userSourceData.roles.filter((role) => {
|
||||
if (role === '') {
|
||||
noRole = true
|
||||
}
|
||||
return role !== ''
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
)
|
||||
).sort()
|
||||
|
||||
// If noRole exists, prefix to array
|
||||
if (noRole) {
|
||||
allRoles = ['', ...allRoles]
|
||||
}
|
||||
|
||||
this.allRoles = allRoles
|
||||
},
|
||||
|
||||
getRoleName(roleName) {
|
||||
if (roleName === '') {
|
||||
return this.$t('visibilityForm.noRole')
|
||||
} else if (roleName.startsWith(DEFAULT_USER_ROLE_PREFIX)) {
|
||||
const userSourceId = parseInt(
|
||||
roleName.split(DEFAULT_USER_ROLE_PREFIX)[1]
|
||||
)
|
||||
const userSource = this.$store.getters['userSource/getUserSourceById'](
|
||||
this.builder,
|
||||
userSourceId
|
||||
)
|
||||
if (userSource) {
|
||||
return this.$t('visibilityForm.rolesAllMembersOf', {
|
||||
name: userSource.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return roleName
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure that the roles of a Page or Element never contain any old or
|
||||
* otherwise invalid roles. This is done by getting the latest valid roles
|
||||
* from the backend and comparing it to the Page/Element roles.
|
||||
*/
|
||||
syncUserRoles() {
|
||||
const validRoles = this.allRoles.filter((role) => {
|
||||
return this.values.roles.includes(role)
|
||||
})
|
||||
|
||||
this.values.roles = validRoles
|
||||
},
|
||||
|
||||
resetElementRoles() {
|
||||
this.values.roles = []
|
||||
this.values.role_type = ROLE_TYPE_ALLOW_ALL
|
||||
},
|
||||
|
||||
selectAllRoles() {
|
||||
this.values.roles = [...this.allRoles]
|
||||
},
|
||||
|
||||
deselectAllRoles() {
|
||||
this.values.roles = []
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { Registerable } from '@baserow/modules/core/registry'
|
||||
import PageSettings from '@baserow/modules/builder/components/page/settings/PageSettings'
|
||||
import PageVisibilitySettings from '@baserow/modules/builder/components/page/settings/PageVisibilitySettings'
|
||||
|
||||
export class PageSettingType extends Registerable {
|
||||
static getType() {
|
||||
|
@ -17,6 +18,10 @@ export class PageSettingType extends Registerable {
|
|||
get component() {
|
||||
return null
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
return this.order
|
||||
}
|
||||
}
|
||||
|
||||
export class PagePageSettingsType extends PageSettingType {
|
||||
|
@ -35,4 +40,30 @@ export class PagePageSettingsType extends PageSettingType {
|
|||
get component() {
|
||||
return PageSettings
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
return 10
|
||||
}
|
||||
}
|
||||
|
||||
export class PageVisibilitySettingsType extends PageSettingType {
|
||||
static getType() {
|
||||
return 'page_visibility'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('pageVisibilitySettingsTypes.pageName')
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return 'iconoir-eye-empty'
|
||||
}
|
||||
|
||||
get component() {
|
||||
return PageVisibilitySettings
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
return 20
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<div>
|
||||
<Toasts></Toasts>
|
||||
<PageContent
|
||||
v-if="canViewPage"
|
||||
:page="page"
|
||||
:path="path"
|
||||
:params="params"
|
||||
|
@ -18,6 +19,8 @@ import { DataProviderType } from '@baserow/modules/core/dataProviderTypes'
|
|||
import Toasts from '@baserow/modules/core/components/toasts/Toasts'
|
||||
import ApplicationBuilderFormulaInput from '@baserow/modules/builder/components/ApplicationBuilderFormulaInput'
|
||||
import _ from 'lodash'
|
||||
import { prefixInternalResolvedUrl } from '@baserow/modules/builder/utils/urlResolution'
|
||||
import { userCanViewPage } from '@baserow/modules/builder/utils/visibility'
|
||||
|
||||
import {
|
||||
getTokenIfEnoughTimeLeft,
|
||||
|
@ -48,6 +51,7 @@ export default {
|
|||
applicationContext: this.applicationContext,
|
||||
}
|
||||
},
|
||||
|
||||
async asyncData({
|
||||
store,
|
||||
params,
|
||||
|
@ -204,6 +208,7 @@ export default {
|
|||
mode,
|
||||
})
|
||||
|
||||
// TODO: This doesn't appear to be doing anything...
|
||||
// And finally select the page to display it
|
||||
await store.dispatch('page/selectById', {
|
||||
builder,
|
||||
|
@ -240,6 +245,17 @@ export default {
|
|||
mode: this.mode,
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Returns true if the current user is allowed to view this page,
|
||||
* otherwise returns false.
|
||||
*/
|
||||
canViewPage() {
|
||||
return userCanViewPage(
|
||||
this.$store.getters['userSourceUser/getUser'](this.builder),
|
||||
this.$store.getters['userSourceUser/isAuthenticated'](this.builder),
|
||||
this.page
|
||||
)
|
||||
},
|
||||
dispatchContext() {
|
||||
return DataProviderType.getAllDataSourceDispatchContext(
|
||||
this.$registry.getAll('builderDataProvider'),
|
||||
|
@ -316,11 +332,41 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
isAuthenticated() {
|
||||
// When the user login or logout, we need to refetch the elements and actions
|
||||
// as they might have changed
|
||||
async isAuthenticated() {
|
||||
// When the user logs in or out, we need to refetch the elements and actions
|
||||
// as they might have changed.
|
||||
this.$store.dispatch('element/fetchPublished', { page: this.page })
|
||||
this.$store.dispatch('workflowAction/fetchPublished', { page: this.page })
|
||||
|
||||
// If the user is on a hidden page, redirect them to the Login page if possible.
|
||||
await this.maybeRedirectUserToLoginPage()
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.maybeRedirectUserToLoginPage()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* If the user does not have access to the current page, redirect them to
|
||||
* the Login page if possible.
|
||||
*/
|
||||
async maybeRedirectUserToLoginPage() {
|
||||
if (!this.canViewPage && this.builder.login_page_id) {
|
||||
const loginPage = await this.$store.getters['page/getById'](
|
||||
this.builder,
|
||||
this.builder.login_page_id
|
||||
)
|
||||
const url = prefixInternalResolvedUrl(
|
||||
loginPage.path,
|
||||
this.builder,
|
||||
'page',
|
||||
this.mode
|
||||
)
|
||||
|
||||
if (url !== this.$router.history.current?.fullPath) {
|
||||
this.$router.push(url)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -66,7 +66,10 @@ import {
|
|||
CustomDomainType,
|
||||
SubDomainType,
|
||||
} from '@baserow/modules/builder/domainTypes'
|
||||
import { PagePageSettingsType } from '@baserow/modules/builder/pageSettingsTypes'
|
||||
import {
|
||||
PagePageSettingsType,
|
||||
PageVisibilitySettingsType,
|
||||
} from '@baserow/modules/builder/pageSettingsTypes'
|
||||
import {
|
||||
TextPathParamType,
|
||||
NumericPathParamType,
|
||||
|
@ -240,6 +243,10 @@ export default (context) => {
|
|||
app.$registry.register('domain', new SubDomainType(context))
|
||||
|
||||
app.$registry.register('pageSettings', new PagePageSettingsType(context))
|
||||
app.$registry.register(
|
||||
'pageSettings',
|
||||
new PageVisibilitySettingsType(context)
|
||||
)
|
||||
|
||||
app.$registry.register('pathParamType', new TextPathParamType(context))
|
||||
app.$registry.register('pathParamType', new NumericPathParamType(context))
|
||||
|
|
41
web-frontend/modules/builder/utils/visibility.js
Normal file
41
web-frontend/modules/builder/utils/visibility.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
VISIBILITY_ALL,
|
||||
ROLE_TYPE_ALLOW_EXCEPT,
|
||||
ROLE_TYPE_DISALLOW_EXCEPT,
|
||||
ROLE_TYPE_ALLOW_ALL,
|
||||
} from '@baserow/modules/builder/constants'
|
||||
|
||||
/**
|
||||
* Evaluates the Page's visibility settings and the user's role. Returns true
|
||||
* if the user is allowed to view the page. Otherwise, returns false.
|
||||
*
|
||||
* @param {Object} user The user object.
|
||||
* @param {Boolean} isAuthenticated Whether the user is authenticated.
|
||||
* @param {Object} page The Page to be evaluated.
|
||||
* @returns {Boolean} True if the user is allowed to view the page, false otherwise.
|
||||
*/
|
||||
export function userCanViewPage(user, isAuthenticated, page) {
|
||||
if (page.visibility === VISIBILITY_ALL) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If visibility is 'logged-in' (i.e. not 'all') *and* the user isn't
|
||||
// authenticated, disallow access.
|
||||
if (!isAuthenticated) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (page.role_type === ROLE_TYPE_ALLOW_EXCEPT) {
|
||||
// Allow if the user's role isn't explicitly excluded
|
||||
return !page.roles.includes(user.role)
|
||||
} else if (page.role_type === ROLE_TYPE_DISALLOW_EXCEPT) {
|
||||
// Allow if the user's role is explicitly included
|
||||
return page.roles.includes(user.role)
|
||||
} else if (page.role_type === ROLE_TYPE_ALLOW_ALL) {
|
||||
// Allow if there are no page level role restrictions
|
||||
return true
|
||||
}
|
||||
|
||||
// Disallow access to the page by default
|
||||
return false
|
||||
}
|
|
@ -35,3 +35,4 @@
|
|||
@import 'page';
|
||||
@import 'data_source_item';
|
||||
@import 'collection_element_header';
|
||||
@import 'side_bar';
|
||||
|
|
|
@ -22,3 +22,12 @@
|
|||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.visibility-form__visibility-all {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.visibility-form__visibility-logged-in {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
/** Only display ellipsis when the user is *not* editing the page name in-place */
|
||||
.side-bar-builder__link-text:not([contenteditable='true']) {
|
||||
@extend %ellipsis;
|
||||
}
|
|
@ -197,9 +197,12 @@
|
|||
@extend %ellipsis;
|
||||
|
||||
color: $palette-neutral-900;
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
padding: 0 32px 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
@ -210,6 +213,7 @@
|
|||
color: $palette-neutral-1200;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line */
|
||||
&--empty::before {
|
||||
@extend %empty;
|
||||
|
||||
|
|
|
@ -209,6 +209,7 @@ export const actions = {
|
|||
forceUpdate({ commit }, { application, data }) {
|
||||
const type = this.$registry.get('application', application.type)
|
||||
data = type.prepareForStoreUpdate(application, data)
|
||||
|
||||
commit('UPDATE_ITEM', { id: application.id, values: data })
|
||||
},
|
||||
/**
|
||||
|
|
163
web-frontend/test/unit/builder/utils/visibility.spec.js
Normal file
163
web-frontend/test/unit/builder/utils/visibility.spec.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
import { userCanViewPage } from '@baserow/modules/builder/utils/visibility'
|
||||
|
||||
import {
|
||||
VISIBILITY_ALL,
|
||||
VISIBILITY_LOGGED_IN,
|
||||
ROLE_TYPE_ALLOW_EXCEPT,
|
||||
ROLE_TYPE_DISALLOW_EXCEPT,
|
||||
ROLE_TYPE_ALLOW_ALL,
|
||||
} from '@baserow/modules/builder/constants'
|
||||
|
||||
describe('userCanViewPage tests', () => {
|
||||
const testCasesVisibilityAll = [
|
||||
{
|
||||
userRole: '',
|
||||
isAuthenticated: false,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_ALL,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: false,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_ALL,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: true,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_ALL,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: true,
|
||||
pageRoles: ['fooRole'],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_ALL,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: true,
|
||||
pageRoles: ['fooRole'],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_EXCEPT,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: true,
|
||||
pageRoles: ['fooRole'],
|
||||
pageRoleType: ROLE_TYPE_DISALLOW_EXCEPT,
|
||||
},
|
||||
]
|
||||
test.each(testCasesVisibilityAll)(
|
||||
'Allow access if visibility is "all", regardless of other page visibility settings.',
|
||||
(testCase) => {
|
||||
const user = { role: testCase.userRole }
|
||||
const page = {
|
||||
visibility: VISIBILITY_ALL,
|
||||
role_type: testCase.pageRoleType,
|
||||
roles: testCase.pageRoles,
|
||||
}
|
||||
const result = userCanViewPage(user, testCase.isAuthenticated, page)
|
||||
expect(result).toEqual(true)
|
||||
}
|
||||
)
|
||||
|
||||
const testCasesVisibilityLoggedIn = [
|
||||
{
|
||||
userRole: '',
|
||||
isAuthenticated: false,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_ALL,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
userRole: '',
|
||||
isAuthenticated: false,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_EXCEPT,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
userRole: '',
|
||||
isAuthenticated: false,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_DISALLOW_EXCEPT,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
userRole: '',
|
||||
isAuthenticated: true,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_ALL,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: true,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_ALL,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: true,
|
||||
pageRoles: ['fooRole'],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_ALL,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
userRole: '',
|
||||
isAuthenticated: true,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_EXCEPT,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: true,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_EXCEPT,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: true,
|
||||
pageRoles: ['fooRole'],
|
||||
pageRoleType: ROLE_TYPE_ALLOW_EXCEPT,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
userRole: '',
|
||||
isAuthenticated: true,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_DISALLOW_EXCEPT,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: true,
|
||||
pageRoles: [],
|
||||
pageRoleType: ROLE_TYPE_DISALLOW_EXCEPT,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
userRole: 'fooRole',
|
||||
isAuthenticated: true,
|
||||
pageRoles: ['fooRole'],
|
||||
pageRoleType: ROLE_TYPE_DISALLOW_EXCEPT,
|
||||
expectedResult: true,
|
||||
},
|
||||
]
|
||||
test.each(testCasesVisibilityLoggedIn)(
|
||||
'Allow access if visibility is "logged-in" and page settings permit access.',
|
||||
(testCase) => {
|
||||
const user = { role: testCase.userRole }
|
||||
const page = {
|
||||
visibility: VISIBILITY_LOGGED_IN,
|
||||
role_type: testCase.pageRoleType,
|
||||
roles: testCase.pageRoles,
|
||||
}
|
||||
const result = userCanViewPage(user, testCase.isAuthenticated, page)
|
||||
expect(result).toEqual(testCase.expectedResult)
|
||||
}
|
||||
)
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue