1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-17 10:22:36 +00:00

Final branch licensing system 2

This commit is contained in:
Bram Wiepjes 2021-10-20 19:28:51 +00:00
parent 9f59c5208f
commit cac27f1628
138 changed files with 6073 additions and 459 deletions
.gitlab-ci.yml
backend
changelog.md
premium

View file

@ -69,5 +69,7 @@ backend-setup:
script:
- pip install -e ./backend
- python -c 'import baserow'
- pip install -e ./premium/backend
- python -c 'import baserow_premium'
- export DJANGO_SETTINGS_MODULE='baserow.config.settings.base'
- timeout --preserve-status 10s gunicorn --workers=1 -b 0.0.0.0:8000 -k uvicorn.workers.UvicornWorker baserow.config.asgi:application

View file

@ -3,7 +3,6 @@ DJANGO_SETTINGS_MODULE = baserow.config.settings.test
python_files = test_*.py
env =
DJANGO_SETTINGS_MODULE = baserow.config.settings.test
ADDITIONAL_APPS = baserow_premium
testpaths =
tests
plugins

View file

@ -25,4 +25,5 @@ celery-redbeat==2.0.0
service-identity==21.1.0
regex==2021.8.3
click==7.1.2
cryptography==3.4.8
antlr4-python3-runtime==4.8.0

View file

@ -11,4 +11,5 @@ argh==0.26.2
black==21.7b0
pyinstrument==4.0.3
pyfakefs==4.5.0
pytest-xdist==2.3.0
pytest-xdist==2.3.0
responses==0.13.4

View file

@ -10,3 +10,12 @@ class SettingsSerializer(serializers.ModelSerializer):
extra_kwargs = {
"allow_new_signups": {"required": False},
}
class InstanceIdSerializer(serializers.ModelSerializer):
class Meta:
model = Settings
fields = ("instance_id",)
extra_kwargs = {
"instance_id": {"read_only": True},
}

View file

@ -1,11 +1,12 @@
from django.urls import re_path
from .views import SettingsView, UpdateSettingsView
from .views import SettingsView, InstanceIdView, UpdateSettingsView
app_name = "baserow.api.settings"
urlpatterns = [
re_path(r"^update/$", UpdateSettingsView.as_view(), name="update"),
re_path(r"^instance-id/$", InstanceIdView.as_view(), name="instance_id"),
re_path(r"^$", SettingsView.as_view(), name="get"),
]

View file

@ -9,7 +9,7 @@ from rest_framework.permissions import AllowAny, IsAdminUser
from baserow.api.decorators import validate_body
from baserow.core.handler import CoreHandler
from .serializers import SettingsSerializer
from .serializers import SettingsSerializer, InstanceIdSerializer
class SettingsView(APIView):
@ -25,14 +25,31 @@ class SettingsView(APIView):
auth=[None],
)
def get(self, request):
"""
Responds with all the admin configured settings.
"""
"""Responds with all the admin configured settings."""
settings = CoreHandler().get_settings()
return Response(SettingsSerializer(settings).data)
class InstanceIdView(APIView):
permission_classes = (IsAdminUser,)
@extend_schema(
tags=["Settings"],
operation_id="get_instance_id",
description="Responds with the self hosted instance id. Only a user with "
"staff permissions can request it.",
responses={
200: InstanceIdSerializer,
},
)
def get(self, request):
"""Responds with the instance id."""
settings = CoreHandler().get_settings()
return Response(InstanceIdSerializer(settings).data)
class UpdateSettingsView(APIView):
permission_classes = (IsAdminUser,)

View file

@ -1,8 +1,18 @@
from baserow.api.user.registries import user_data_registry
from .serializers import UserSerializer
def jwt_response_payload_handler(token, user=None, request=None, issued_at=None):
return {
payload = {
"token": token,
"user": UserSerializer(user, context={"request": request}).data,
}
# Update the payload with the additional user data that must be added. The
# `user_data_registry` contains instances that want to add additional information
# to this payload.
payload.update(**user_data_registry.get_all_user_data(user, request))
return payload

View file

@ -0,0 +1,73 @@
from baserow.core.registry import Instance, Registry
class UserDataType(Instance):
"""
The user data type can be used to inject an additional payload to the API
JWT response. This is the response when a user authenticates or refreshes his
token. The returned dict of the `get_user_data` method is added to the payload
under the key containing the type name.
Example:
class TestUserDataType(UserDataType):
type = "test"
def get_user_data(user, request):
return {"test": "value"}
user_data_registry.register(TestUserDataType())
Will result into the following response when the user authenticates:
{
"token": "eyJ....",
"user: {
"id": 1,
...
},
"test": {
"test": "value"
}
}
"""
def get_user_data(self, user, request) -> dict:
"""
Should return a dict containing the additional information that must be added
to the response payload after the user authenticates.
:param user: The related user that just authenticated.
:type user: User
:param request: The request when the user authenticated.
:type request: Request
:return: a dict containing the user data that must be added to the response.
"""
raise NotImplementedError(
"The get_user_data must be implemented and should return a dict."
)
class UserDataRegistry(Registry):
name = "api_user_data"
def get_all_user_data(self, user, request) -> dict:
"""
Collects the additional user data of all the registered user data type
instances.
:param user: The user that just authenticated.
:type user: User
:param request: The request when the user authenticated.
:type request: Request
:return: a dict containing all additional user data payload for all the
registered instances.
"""
return {
key: value.get_user_data(user, request)
for key, value in self.registry.items()
}
user_data_registry = UserDataRegistry()

View file

@ -26,6 +26,7 @@ from baserow.api.groups.invitations.errors import (
ERROR_GROUP_INVITATION_EMAIL_MISMATCH,
)
from baserow.api.schemas import get_error_schema
from baserow.api.user.registries import user_data_registry
from baserow.core.exceptions import (
BaseURLHostnameNotAllowed,
GroupInvitationEmailMismatch,
@ -195,6 +196,7 @@ class UserView(APIView):
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
response.update(token=token)
response.update(**user_data_registry.get_all_user_data(user, request))
return Response(response)

View file

@ -31,6 +31,7 @@ INSTALLED_APPS = [
"baserow.api",
"baserow.ws",
"baserow.contrib.database",
"baserow_premium",
]
ADDITIONAL_APPS = os.getenv("ADDITIONAL_APPS", None)

View file

@ -84,6 +84,10 @@ class ExportHandler:
:return: The created export job.
"""
exporter_type = export_options.pop("exporter_type")
exporter = table_exporter_registry.get(exporter_type)
exporter.before_job_create(user, table, view, export_options)
table.database.group.has_user(user, raise_error=True)
if view and view.table.id != table.id:
@ -91,8 +95,6 @@ class ExportHandler:
_cancel_unfinished_jobs(user)
exporter_type = export_options.pop("exporter_type")
_raise_if_invalid_view_or_table_for_exporter(exporter_type, view)
job = ExportJob.objects.create(

View file

@ -19,6 +19,21 @@ class TableExporter(Instance, ABC):
without specifying a particular view.
"""
def before_job_create(self, user, table, view, export_options):
"""
This method is called just before an export job is created. It can be used to
do some additional checking of the provided values.
:param user: The user that requested the creation of the job.
:type user: User
:param table: The table that must be exported.
:type table: Table
:param view: The view that must be exported.
:type view: None or View
:param export_options: The additional user provided export options.
:type export_options: dict
"""
@property
@abstractmethod
def file_extension(self) -> str:

View file

@ -0,0 +1,36 @@
from django.db import DEFAULT_DB_ALIAS
from django.db.transaction import Atomic, get_connection
class LockedAtomicTransaction(Atomic):
"""
Does a atomic transaction, but also locks the entire table for any transactions,
for the duration of this transaction. Although this is the only way to avoid
concurrency issues in certain situations, it should be used with caution,
since it has impacts on performance, for obvious reasons...
"""
def __init__(self, model, using=None, savepoint=True, durable=False):
if using is None:
using = DEFAULT_DB_ALIAS
super().__init__(using, savepoint, durable)
self.model = model
def __enter__(self):
super(LockedAtomicTransaction, self).__enter__()
cursor = None
try:
cursor = get_connection(self.using).cursor()
cursor.execute(
"LOCK TABLE {db_table_name}".format(
db_table_name=self.model._meta.db_table
)
)
finally:
if cursor and not cursor.closed:
cursor.close()
def __exit__(self, *args, **kwargs):
return super().__exit__(*args, **kwargs)

View file

@ -95,8 +95,9 @@ class CoreHandler:
if not settings_instance:
settings_instance = self.get_settings()
for name, value in kwargs.items():
setattr(settings_instance, name, value)
settings_instance = set_allowed_attrs(
kwargs, ["allow_new_signups"], settings_instance
)
settings_instance.save()
return settings_instance

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.6 on 2021-10-07 19:26
from django.db import migrations, models
import secrets
class Migration(migrations.Migration):
dependencies = [
("core", "0010_fix_trash_constraint"),
]
operations = [
migrations.AddField(
model_name="settings",
name="instance_id",
field=models.SlugField(default=secrets.token_urlsafe),
),
]

View file

@ -1,3 +1,5 @@
import secrets
from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
@ -55,6 +57,7 @@ class Settings(models.Model):
change. This table can only contain a single row.
"""
instance_id = models.SlugField(default=secrets.token_urlsafe)
allow_new_signups = models.BooleanField(
default=True,
help_text="Indicates whether new users can create a new account when signing "

View file

@ -21,7 +21,7 @@ def _parse_date(date):
return parse_date(date)
def setup_interesting_test_table(data_fixture):
def setup_interesting_test_table(data_fixture, user_kwargs=None):
"""
Constructs a testing table with every field type, their sub types and any other
interesting baserow edge cases worth testing when writing a comphensive "does this
@ -31,7 +31,10 @@ def setup_interesting_test_table(data_fixture):
:return:
"""
user = data_fixture.create_user()
if not user_kwargs:
user_kwargs = {}
user = data_fixture.create_user(**user_kwargs)
database = data_fixture.create_database_application(user=user)
table = data_fixture.create_database_table(database=database, user=user)
link_table = data_fixture.create_database_table(database=database, user=user)

View file

@ -0,0 +1,53 @@
import os
import pytest
@pytest.fixture
def data_fixture():
from .fixtures import Fixtures
return Fixtures()
@pytest.fixture()
def api_client():
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture()
def environ():
original_env = os.environ.copy()
yield os.environ
for key, value in original_env.items():
os.environ[key] = value
# We reuse this file in the premium backend folder, if you run a pytest session over
# plugins and the core at the same time pytest will crash if this called multiple times.
def pytest_addoption(parser):
# Unfortunately a simple decorator doesn't work here as pytest is doing some
# exciting reflection of sorts over this function and crashes if it is wrapped.
if not hasattr(pytest_addoption, "already_run"):
parser.addoption(
"--runslow", action="store_true", default=False, help="run slow tests"
)
pytest_addoption.already_run = True
def pytest_configure(config):
if not hasattr(pytest_configure, "already_run"):
config.addinivalue_line("markers", "slow: mark test as slow to run")
pytest_configure.already_run = True
def pytest_collection_modifyitems(config, items):
if config.getoption("--runslow"):
# --runslow given in cli: do not skip slow tests
return
skip_slow = pytest.mark.skip(reason="need --runslow option to run")
for item in items:
if "slow" in item.keywords:
item.add_marker(skip_slow)

View file

@ -1,6 +1,11 @@
import pytest
from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN
from rest_framework.status import (
HTTP_200_OK,
HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED,
HTTP_403_FORBIDDEN,
)
from django.shortcuts import reverse
@ -13,6 +18,7 @@ def test_get_settings(api_client):
response = api_client.get(reverse("api:settings:get"))
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert "instance_id" not in response_json
assert response_json["allow_new_signups"] is True
settings = Settings.objects.first()
@ -22,9 +28,46 @@ def test_get_settings(api_client):
response = api_client.get(reverse("api:settings:get"))
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert "instance_id" not in response_json
assert response_json["allow_new_signups"] is False
@pytest.mark.django_db
def test_get_instance_id(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(is_staff=True)
user_2, token_2 = data_fixture.create_user_and_token()
response = api_client.get(reverse("api:settings:instance_id"))
assert response.status_code == HTTP_401_UNAUTHORIZED
response = api_client.get(
reverse("api:settings:instance_id"),
HTTP_AUTHORIZATION=f"JWT {token_2}",
)
assert response.status_code == HTTP_403_FORBIDDEN
assert CoreHandler().get_settings().allow_new_signups is True
response = api_client.get(
reverse("api:settings:instance_id"),
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert len(response_json["instance_id"]) > 32
settings = Settings.objects.first()
settings.allow_new_signups = False
settings.save()
response = api_client.get(
reverse("api:settings:instance_id"),
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["instance_id"] == settings.instance_id
@pytest.mark.django_db
def test_update_settings(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(is_staff=True)
@ -50,6 +93,15 @@ def test_update_settings(api_client, data_fixture):
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
assert response_json["detail"]["allow_new_signups"][0]["code"] == "invalid"
response = api_client.patch(
reverse("api:settings:update"),
{"instance_id": "test"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
assert CoreHandler().get_settings().instance_id != "test"
response = api_client.patch(
reverse("api:settings:update"),
{"allow_new_signups": False},
@ -59,6 +111,7 @@ def test_update_settings(api_client, data_fixture):
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["allow_new_signups"] is False
assert "instance_id" not in response_json
assert CoreHandler().get_settings().allow_new_signups is False
response = api_client.patch(
@ -70,4 +123,5 @@ def test_update_settings(api_client, data_fixture):
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["allow_new_signups"] is False
assert "instance_id" not in response_json
assert CoreHandler().get_settings().allow_new_signups is False

View file

@ -1,12 +1,19 @@
import os
import pytest
from unittest.mock import patch
from freezegun import freeze_time
from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND
from rest_framework.status import (
HTTP_200_OK,
HTTP_201_CREATED,
HTTP_400_BAD_REQUEST,
HTTP_404_NOT_FOUND,
)
from django.contrib.auth import get_user_model
from django.shortcuts import reverse
from django.conf import settings
from baserow.api.user.registries import user_data_registry, UserDataType
from baserow.contrib.database.models import Database, Table
from baserow.core.handler import CoreHandler
from baserow.core.models import Group, GroupUser
@ -642,3 +649,46 @@ def test_dashboard(data_fixture, client):
assert response_json["group_invitations"][0]["group"] == "Test1"
assert response_json["group_invitations"][0]["message"] == invitation_1.message
assert "created_on" in response_json["group_invitations"][0]
@pytest.mark.django_db
def test_additional_user_data(api_client, data_fixture):
class TmpUserDataType(UserDataType):
type = "type"
def get_user_data(self, user, request) -> dict:
return True
plugin_mock = TmpUserDataType()
with patch.dict(user_data_registry.registry, {"tmp": plugin_mock}):
response = api_client.post(
reverse("api:user:index"),
{
"name": "Test1",
"email": "test@test.nl",
"password": "thisIsAValidPassword",
"authenticate": True,
},
format="json",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json["tmp"] is True
response = api_client.post(
reverse("api:user:token_auth"),
{"username": "test@test.nl", "password": "thisIsAValidPassword"},
format="json",
)
response_json = response.json()
assert response.status_code == HTTP_201_CREATED
assert response_json["tmp"] is True
response = api_client.post(
reverse("api:user:token_refresh"),
{"token": response_json["token"]},
format="json",
)
response_json = response.json()
assert response.status_code == HTTP_201_CREATED
assert response_json["tmp"] is True

View file

@ -8,7 +8,7 @@ from baserow.contrib.database.api.rows.serializers import (
)
from baserow.contrib.database.fields.models import SelectOption
from baserow.contrib.database.fields.registries import field_type_registry
from tests.test_utils import setup_interesting_test_table
from baserow.test_utils.helpers import setup_interesting_test_table
@pytest.mark.django_db

View file

@ -16,7 +16,7 @@ from baserow.contrib.database.rows.registries import (
row_metadata_registry,
)
from baserow.contrib.database.views.models import GridView
from tests.test_utils import register_instance_temporarily
from baserow.test_utils.helpers import register_instance_temporarily
@pytest.mark.django_db

View file

@ -38,7 +38,7 @@ from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.rows.handler import RowHandler
from baserow.contrib.database.views.exceptions import ViewNotInTable
from baserow.contrib.database.views.models import GridView, GridViewFieldOptions
from tests.test_utils import setup_interesting_test_table
from baserow.test_utils.helpers import setup_interesting_test_table
def _parse_datetime(datetime):

View file

@ -15,7 +15,7 @@ from baserow.contrib.database.fields.models import (
)
from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.rows.handler import RowHandler
from tests.test_utils import setup_interesting_test_table
from baserow.test_utils.helpers import setup_interesting_test_table
@pytest.mark.django_db
@ -369,7 +369,7 @@ def test_email_field_type(data_fixture):
@pytest.mark.django_db
@override_settings(debug=True)
@override_settings(DEBUG=True)
def test_phone_number_field_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)

View file

@ -12,7 +12,7 @@ from baserow.contrib.database.rows.registries import (
RowMetadataType,
row_metadata_registry,
)
from tests.test_utils import register_instance_temporarily
from baserow.test_utils.helpers import register_instance_temporarily
@pytest.mark.django_db(transaction=True)

View file

@ -0,0 +1,26 @@
import pytest
from django.db import connection
from django.test.utils import override_settings
from baserow.core.db import LockedAtomicTransaction
from baserow.core.models import Settings
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_locked_atomic_transaction(api_client, data_fixture):
def is_locked(model):
with connection.cursor() as cursor:
table_name = model._meta.db_table
cursor.execute(
"select count(*) from pg_locks l join pg_class t on l.relation = "
"t.oid WHERE relname = %(table_name)s;",
{"table_name": table_name},
)
return cursor.fetchone()[0] > 0
assert not is_locked(Settings)
with LockedAtomicTransaction(Settings):
assert is_locked(Settings)

View file

@ -45,6 +45,7 @@ from baserow.core.user_files.models import UserFile
def test_get_settings():
settings = CoreHandler().get_settings()
assert isinstance(settings, Settings)
assert len(settings.instance_id) > 32
assert settings.allow_new_signups is True
@ -56,10 +57,14 @@ def test_update_settings(data_fixture):
with pytest.raises(IsNotAdminError):
CoreHandler().update_settings(user_2, allow_new_signups=False)
settings = CoreHandler().update_settings(user_1, allow_new_signups=False)
settings = CoreHandler().update_settings(
user_1, allow_new_signups=False, instance_id="test"
)
assert settings.allow_new_signups is False
assert settings.instance_id != "test"
settings = Settings.objects.all().first()
assert settings.instance_id != "test"
assert settings.allow_new_signups is False

View file

@ -9,7 +9,7 @@ from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.management.commands.fill_table import fill_table
from baserow.contrib.database.rows.handler import RowHandler
from baserow.core.trash.handler import TrashHandler
from tests.test_utils import setup_interesting_test_table
from baserow.test_utils.helpers import setup_interesting_test_table
@pytest.mark.django_db

View file

@ -4,7 +4,7 @@ from pyinstrument import Profiler
from baserow.contrib.database.management.commands.fill_table import fill_table
from baserow.core.models import TrashEntry
from baserow.core.trash.handler import TrashHandler
from tests.test_utils import setup_interesting_test_table
from baserow.test_utils.helpers import setup_interesting_test_table
@pytest.mark.django_db

View file

@ -1,55 +1,5 @@
from __future__ import print_function
import os
import pytest
@pytest.fixture
def data_fixture():
from .fixtures import Fixtures
return Fixtures()
@pytest.fixture()
def api_client():
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture()
def environ():
original_env = os.environ.copy()
yield os.environ
for key, value in original_env.items():
os.environ[key] = value
# We reuse this file in the premium backend folder, if you run a pytest session over
# plugins and the core at the same time pytest will crash if this called multiple times.
def pytest_addoption(parser):
# Unfortunately a simple decorator doesn't work here as pytest is doing some
# exciting reflection of sorts over this function and crashes if it is wrapped.
if not hasattr(pytest_addoption, "already_run"):
parser.addoption(
"--runslow", action="store_true", default=False, help="run slow tests"
)
pytest_addoption.already_run = True
def pytest_configure(config):
if not hasattr(pytest_configure, "already_run"):
config.addinivalue_line("markers", "slow: mark test as slow to run")
pytest_configure.already_run = True
def pytest_collection_modifyitems(config, items):
if config.getoption("--runslow"):
# --runslow given in cli: do not skip slow tests
return
skip_slow = pytest.mark.skip(reason="need --runslow option to run")
for item in items:
if "slow" in item.keywords:
item.add_marker(skip_slow)
# noinspection PyUnresolvedReferences
from baserow.test_utils.pytest_conftest import * # noqa: F403, F401

View file

@ -8,6 +8,7 @@
env variable. If using MEDIA_PORT you now need to set MEDIA_URL also.
* **Breaking Change**: Baserow's `docker-compose.yml` container names have changed to
no longer be hardcoded to prevent naming clashes.
* Added a licensing system for the premium version.
* Fixed bug where it was possible to create duplicate trash entries.
## Released (2021-10-05)

0
premium/__init__.py Normal file
View file

View file

19
premium/backend/setup.py Normal file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env python
import os
from setuptools import find_packages, setup
PROJECT_DIR = os.path.dirname(__file__)
setup(
name="baserow_premium",
url="https://baserow.io",
author="Bram Wiepjes (Baserow)",
author_email="bram@baserow.io",
platforms=["linux"],
package_dir={"": "src"},
packages=find_packages("src"),
include_package_data=True,
)

View file

@ -2,6 +2,7 @@ from baserow.core.exceptions import IsNotAdminError
from baserow.core.signals import group_deleted
from baserow.core.trash.handler import TrashHandler
from baserow_premium.admin.groups.exceptions import CannotDeleteATemplateGroupError
from baserow_premium.license.handler import check_active_premium_license
class GroupsAdminHandler:
@ -16,6 +17,8 @@ class GroupsAdminHandler:
:raises IsNotAdminError: If the user is not admin or staff.
"""
check_active_premium_license(user)
if not user.is_staff:
raise IsNotAdminError()

View file

@ -11,6 +11,7 @@ from baserow_premium.admin.users.exceptions import (
CannotDeleteYourselfException,
UserDoesNotExistException,
)
from baserow_premium.license.handler import check_active_premium_license
User = get_user_model()
@ -44,6 +45,7 @@ class UserAdminHandler:
a valid password.
"""
check_active_premium_license(requesting_user)
self._raise_if_not_permitted(requesting_user)
self._raise_if_locking_self_out_of_admin(
is_active, is_staff, requesting_user, user_id
@ -101,6 +103,7 @@ class UserAdminHandler:
UnknownUserException.
"""
check_active_premium_license(requesting_user)
self._raise_if_not_permitted(requesting_user)
if requesting_user.id == user_id:

View file

@ -12,6 +12,7 @@ from baserow.api.decorators import accept_timezone
from baserow.core.models import Group, Application
from baserow_premium.admin.dashboard.handler import AdminDashboardHandler
from baserow_premium.license.handler import check_active_premium_license
from .serializers import AdminDashboardSerializer
@ -47,6 +48,8 @@ class AdminDashboardView(APIView):
last 30 days is also included.
"""
check_active_premium_license(request.user)
handler = AdminDashboardHandler()
total_users = User.objects.filter(is_active=True).count()
total_groups = Group.objects.all().count()

View file

@ -16,8 +16,9 @@ from baserow.core.exceptions import GroupDoesNotExist
from baserow_premium.admin.groups.exceptions import CannotDeleteATemplateGroupError
from baserow_premium.api.admin.views import AdminListingView
from baserow_premium.admin.groups.handler import GroupsAdminHandler
from .errors import ERROR_CANNOT_DELETE_A_TEMPLATE_GROUP
from baserow_premium.license.handler import check_active_premium_license
from .errors import ERROR_CANNOT_DELETE_A_TEMPLATE_GROUP
from .serializers import GroupsAdminResponseSerializer
@ -49,8 +50,9 @@ class GroupsAdminView(AdminListingView):
"groups", serializer_class, search_fields, sort_field_mapping
),
)
def get(self, *args, **kwargs):
return super().get(*args, **kwargs)
def get(self, request):
check_active_premium_license(request.user)
return super().get(request)
class GroupAdminView(APIView):

View file

@ -27,6 +27,7 @@ from baserow_premium.admin.users.handler import UserAdminHandler
from django.contrib.auth import get_user_model
from baserow_premium.api.admin.views import AdminListingView
from baserow_premium.license.handler import check_active_premium_license
User = get_user_model()
@ -58,8 +59,9 @@ class UsersAdminView(AdminListingView):
"users", serializer_class, search_fields, sort_field_mapping
),
)
def get(self, *args, **kwargs):
return super().get(*args, **kwargs)
def get(self, request):
check_active_premium_license(request.user)
return super().get(request)
class UserAdminView(APIView):

View file

@ -0,0 +1,45 @@
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND
ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST = (
"ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST",
HTTP_404_NOT_FOUND,
"The requested license does not exist. Please use the internal `id` and not the "
"`license_id`.",
)
ERROR_INVALID_PREMIUM_LICENSE = (
"ERROR_INVALID_PREMIUM_LICENSE",
HTTP_400_BAD_REQUEST,
"The provided license is invalid.",
)
ERROR_UNSUPPORTED_PREMIUM_LICENSE = (
"ERROR_UNSUPPORTED_PREMIUM_LICENSE",
HTTP_400_BAD_REQUEST,
"This license version is not supported. You probably need to update your Baserow "
"installation.",
)
ERROR_PREMIUM_LICENSE_INSTANCE_ID_MISMATCH = (
"ERROR_PREMIUM_LICENSE_INSTANCE_ID_MISMATCH",
HTTP_400_BAD_REQUEST,
"The `instance_id` of the license doesn't match the instance id of Baserow copy.",
)
ERROR_PREMIUM_LICENSE_ALREADY_EXISTS = (
"ERROR_PREMIUM_LICENSE_ALREADY_EXISTS",
HTTP_400_BAD_REQUEST,
"The provided premium license already exists.",
)
ERROR_PREMIUM_LICENSE_HAS_EXPIRED = (
"ERROR_PREMIUM_LICENSE_HAS_EXPIRED",
HTTP_400_BAD_REQUEST,
"The provided premium license has already expired.",
)
ERROR_USER_ALREADY_ON_PREMIUM_LICENSE = (
"ERROR_USER_ALREADY_IN_PREMIUM_LICENSE",
HTTP_400_BAD_REQUEST,
"The provided user is already on the premium license.",
)
ERROR_NO_SEATS_LEFT_IN_PREMIUM_LICENSE = (
"ERROR_NO_SEATS_LEFT_IN_PREMIUM_LICENSE",
HTTP_400_BAD_REQUEST,
"Can't add the user because there are not seats left in the license.",
)

View file

@ -0,0 +1,100 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.openapi import OpenApiTypes
from baserow_premium.license.models import License
User = get_user_model()
class PremiumLicenseSerializer(serializers.ModelSerializer):
license_id = serializers.CharField(help_text="Unique identifier of the license.")
is_active = serializers.BooleanField(
help_text="Indicates if the backend deems the license valid."
)
valid_from = serializers.DateTimeField(
help_text="From which timestamp the license becomes active."
)
valid_through = serializers.DateTimeField(
help_text="Until which timestamp the license is active."
)
seats_taken = serializers.SerializerMethodField(
help_text="The amount of users that are currently using the license."
)
seats = serializers.IntegerField(
help_text="The maximum amount of users that can use the license."
)
product_code = serializers.CharField(
help_text="The product code that indicates what the license unlocks."
)
issued_on = serializers.DateTimeField(
help_text="The date when the license was issued. It could be that a new "
"license is issued with the same `license_id` because it was updated. In that "
"case, the one that has been issued last should be used."
)
issued_to_email = serializers.EmailField(
help_text="Indicates to which email address the license has been issued."
)
issued_to_name = serializers.CharField(
help_text="Indicates to whom the license has been issued."
)
class Meta:
model = License
fields = (
"id",
"license_id",
"is_active",
"last_check",
"valid_from",
"valid_through",
"seats_taken",
"seats",
"product_code",
"issued_on",
"issued_to_email",
"issued_to_name",
)
@extend_schema_field(OpenApiTypes.INT)
def get_seats_taken(self, obj):
return (
obj.seats_taken if hasattr(obj, "seats_taken") else obj.users.all().count()
)
class RegisterPremiumLicenseSerializer(serializers.Serializer):
license = serializers.CharField(help_text="The license that you want to register.")
class PremiumLicenseUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("id", "first_name", "email")
class PremiumLicenseWithUsersSerializer(PremiumLicenseSerializer):
users = serializers.SerializerMethodField()
class Meta(PremiumLicenseSerializer.Meta):
fields = PremiumLicenseSerializer.Meta.fields + ("users",)
@extend_schema_field(PremiumLicenseUserSerializer(many=True))
def get_users(self, object):
users = [user.user for user in object.users.all()]
return PremiumLicenseUserSerializer(users, many=True).data
class PremiumLicenseUserLookupSerializer(serializers.ModelSerializer):
value = serializers.SerializerMethodField(
help_text="The name and the email " "address of the user."
)
class Meta:
model = User
fields = ("id", "value")
@extend_schema_field(OpenApiTypes.STR)
def get_value(self, object):
return f"{object.first_name} ({object.email})"

View file

@ -0,0 +1,44 @@
from django.conf.urls import re_path
from .views import (
AdminLicensesView,
AdminLicenseView,
AdminLicenseFillSeatsView,
AdminRemoveAllUsersFromLicenseView,
AdminLicenseUserView,
AdminLicenseLookupUsersView,
AdminCheckLicense,
)
app_name = "baserow_premium.api.license"
urlpatterns = [
re_path(r"^$", AdminLicensesView.as_view(), name="list"),
re_path(r"^(?P<id>[0-9]+)/$", AdminLicenseView.as_view(), name="item"),
re_path(
r"^(?P<id>[0-9]+)/lookup-users/$",
AdminLicenseLookupUsersView.as_view(),
name="lookup_users",
),
re_path(
r"^(?P<id>[0-9]+)/fill-seats/$",
AdminLicenseFillSeatsView.as_view(),
name="fill_seats",
),
re_path(
r"^(?P<id>[0-9]+)/remove-all-users/$",
AdminRemoveAllUsersFromLicenseView.as_view(),
name="remove_all_users",
),
re_path(
r"^(?P<id>[0-9]+)/check/$",
AdminCheckLicense.as_view(),
name="check",
),
re_path(
r"^(?P<id>[0-9]+)/(?P<user_id>[0-9]+)/$",
AdminLicenseUserView.as_view(),
name="user",
),
]

View file

@ -0,0 +1,458 @@
from django.contrib.auth import get_user_model
from django.db.models import Count, Q
from django.conf import settings
from django.db import transaction
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, OpenApiParameter
from rest_framework.response import Response
from rest_framework.permissions import IsAdminUser
from rest_framework.views import APIView
from baserow.api.schemas import get_error_schema
from baserow.api.decorators import validate_body, map_exceptions
from baserow.api.user.errors import ERROR_USER_NOT_FOUND
from baserow.api.pagination import PageNumberPagination
from baserow.api.serializers import get_example_pagination_serializer_class
from baserow.core.db import LockedAtomicTransaction
from baserow_premium.license.models import License
from baserow_premium.license.handler import (
register_license,
remove_license,
check_licenses,
add_user_to_license,
remove_user_from_license,
fill_remaining_seats_of_license,
remove_all_users_from_license,
)
from baserow_premium.license.exceptions import (
InvalidPremiumLicenseError,
UnsupportedPremiumLicenseError,
PremiumLicenseInstanceIdMismatchError,
PremiumLicenseHasExpired,
PremiumLicenseAlreadyExists,
UserAlreadyOnPremiumLicenseError,
NoSeatsLeftInPremiumLicenseError,
)
from .errors import (
ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST,
ERROR_INVALID_PREMIUM_LICENSE,
ERROR_UNSUPPORTED_PREMIUM_LICENSE,
ERROR_PREMIUM_LICENSE_INSTANCE_ID_MISMATCH,
ERROR_PREMIUM_LICENSE_HAS_EXPIRED,
ERROR_PREMIUM_LICENSE_ALREADY_EXISTS,
ERROR_USER_ALREADY_ON_PREMIUM_LICENSE,
ERROR_NO_SEATS_LEFT_IN_PREMIUM_LICENSE,
)
from .serializers import (
PremiumLicenseSerializer,
RegisterPremiumLicenseSerializer,
PremiumLicenseUserSerializer,
PremiumLicenseWithUsersSerializer,
PremiumLicenseUserLookupSerializer,
)
User = get_user_model()
class AdminLicensesView(APIView):
permission_classes = (IsAdminUser,)
@extend_schema(
tags=["Admin"],
operation_id="admin_premium_licenses",
description=(
"Lists all the valid licenses that are registered to this instance. A "
"premium license can be used to unlock the premium features for a fixed "
"amount of users. More information about self hosted licenses can be "
"found on our pricing page https://baserow.io/pricing."
),
responses={
200: PremiumLicenseSerializer(many=True),
},
)
def get(self, request):
licenses = License.objects.all().annotate(seats_taken=Count("users"))
# We will sort the items in memory because we don't have access to the
# `is_active` and `valid_from` property after the instance has been
# generated. This is because it needs to decode the license. We first want to
# show the active licenses because those are more important.
licenses = sorted(licenses, key=lambda x: (not x.is_active, x.valid_from))
return Response(PremiumLicenseSerializer(licenses, many=True).data)
@extend_schema(
tags=["Admin"],
operation_id="admin_register_premium_license",
description=(
"Registers a new license. After registering you can assign users to the "
"license that will be able to use the premium features while the license "
"is active. If an existing license with the same `license_id` already "
"exists and the provided license has been issued later than that one, "
"the existing one will be upgraded."
),
request=RegisterPremiumLicenseSerializer,
responses={
200: PremiumLicenseSerializer,
400: get_error_schema(
[
"ERROR_INVALID_PREMIUM_LICENSE",
"ERROR_UNSUPPORTED_PREMIUM_LICENSE",
"ERROR_PREMIUM_LICENSE_INSTANCE_ID_MISMATCH",
"ERROR_PREMIUM_LICENSE_HAS_EXPIRED",
"ERROR_PREMIUM_LICENSE_ALREADY_EXISTS",
]
),
},
)
@validate_body(RegisterPremiumLicenseSerializer)
@map_exceptions(
{
InvalidPremiumLicenseError: ERROR_INVALID_PREMIUM_LICENSE,
UnsupportedPremiumLicenseError: ERROR_UNSUPPORTED_PREMIUM_LICENSE,
PremiumLicenseInstanceIdMismatchError: (
ERROR_PREMIUM_LICENSE_INSTANCE_ID_MISMATCH
),
PremiumLicenseHasExpired: ERROR_PREMIUM_LICENSE_HAS_EXPIRED,
PremiumLicenseAlreadyExists: ERROR_PREMIUM_LICENSE_ALREADY_EXISTS,
}
)
def post(self, request, data):
with LockedAtomicTransaction(License):
license_object = register_license(request.user, data["license"])
return Response(PremiumLicenseSerializer(license_object).data)
class AdminLicenseView(APIView):
permission_classes = (IsAdminUser,)
@extend_schema(
parameters=[
OpenApiParameter(
name="id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The internal identifier of the license.",
),
],
tags=["Admin"],
operation_id="admin_get_premium_license",
description=(
"Responds with detailed information about the license related to the "
"provided parameter."
),
responses={
200: PremiumLicenseWithUsersSerializer,
404: get_error_schema(["ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"]),
},
)
@map_exceptions({License.DoesNotExist: ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST})
def get(self, request, id):
license = License.objects.prefetch_related("users__user").get(pk=id)
return Response(PremiumLicenseWithUsersSerializer(license).data)
@extend_schema(
parameters=[
OpenApiParameter(
name="id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The internal identifier of the license, this is `id` and "
"not `license_id`.",
),
],
tags=["Admin"],
operation_id="admin_remove_premium_license",
description=(
"Removes the existing license related to the provided parameter. If the "
"license is active, then all the users that are using the license will "
"lose access to the premium version."
),
responses={
204: None,
404: get_error_schema(["ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"]),
},
)
@map_exceptions({License.DoesNotExist: ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST})
@transaction.atomic
def delete(self, request, id):
license = License.objects.get(pk=id)
remove_license(request.user, license)
return Response(status=204)
class AdminLicenseUserView(APIView):
permission_classes = (IsAdminUser,)
@extend_schema(
parameters=[
OpenApiParameter(
name="id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The internal identifier of the license, this is `id` and "
"not `license_id`.",
),
OpenApiParameter(
name="user_id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The ID of the user that must be added to the license.",
),
],
tags=["Admin"],
operation_id="admin_add_user_to_premium_license",
description=(
"Adds the user related to the provided parameter and to the license "
"related to the parameter. This only happens if there are enough seats "
"left on the license and if the user is not already on the license."
),
request=None,
responses={
200: PremiumLicenseUserSerializer,
400: get_error_schema(
[
"ERROR_USER_ALREADY_ON_PREMIUM_LICENSE",
"ERROR_NO_SEATS_LEFT_IN_PREMIUM_LICENSE",
]
),
404: get_error_schema(
["ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST", "ERROR_USER_NOT_FOUND"]
),
},
)
@map_exceptions(
{
License.DoesNotExist: ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST,
User.DoesNotExist: ERROR_USER_NOT_FOUND,
UserAlreadyOnPremiumLicenseError: ERROR_USER_ALREADY_ON_PREMIUM_LICENSE,
NoSeatsLeftInPremiumLicenseError: ERROR_NO_SEATS_LEFT_IN_PREMIUM_LICENSE,
}
)
@transaction.atomic
def post(self, request, id, user_id):
license = License.objects.select_for_update().get(pk=id)
user = User.objects.get(pk=user_id)
add_user_to_license(request.user, license, user)
return Response(PremiumLicenseUserSerializer(user).data)
@extend_schema(
parameters=[
OpenApiParameter(
name="id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The internal identifier of the license, this is `id` and "
"not `license_id`.",
),
OpenApiParameter(
name="user_id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The ID of the user that must be removed from the license.",
),
],
tags=["Admin"],
operation_id="admin_remove_user_from_premium_license",
description=(
"Removes the user related to the provided parameter and to the license "
"related to the parameter. This only happens if the user is on the "
"license, otherwise nothing will happen."
),
request=None,
responses={
204: None,
404: get_error_schema(
["ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST", "ERROR_USER_NOT_FOUND"]
),
},
)
@map_exceptions(
{
License.DoesNotExist: ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST,
User.DoesNotExist: ERROR_USER_NOT_FOUND,
}
)
@transaction.atomic
def delete(self, request, id, user_id):
license = License.objects.select_for_update().get(pk=id)
user = User.objects.get(pk=user_id)
remove_user_from_license(request.user, license, user)
return Response(status=204)
class AdminLicenseFillSeatsView(APIView):
permission_classes = (IsAdminUser,)
@extend_schema(
parameters=[
OpenApiParameter(
name="id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The internal identifier of the license, this is `id` and "
"not `license_id`.",
),
],
tags=["Admin"],
operation_id="admin_fill_remaining_seats_of_premium_license",
description=(
"Fills the remaining empty seats of the license with the first users that "
"are found."
),
request=None,
responses={
200: PremiumLicenseUserSerializer(many=True),
404: get_error_schema(["ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"]),
},
)
@map_exceptions({License.DoesNotExist: ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST})
@transaction.atomic
def post(self, request, id):
license = License.objects.get(pk=id)
license_users = fill_remaining_seats_of_license(request.user, license)
users = [license_user.user for license_user in license_users]
return Response(PremiumLicenseUserSerializer(users, many=True).data)
class AdminRemoveAllUsersFromLicenseView(APIView):
permission_classes = (IsAdminUser,)
@extend_schema(
parameters=[
OpenApiParameter(
name="id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The internal identifier of the license, this is `id` and "
"not `license_id`.",
),
],
tags=["Admin"],
operation_id="admin_remove_all_users_from_premium_license",
description=(
"Removes all the users the users that are on the license. This will "
"empty all the seats."
),
request=None,
responses={
204: None,
404: get_error_schema(["ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"]),
},
)
@map_exceptions({License.DoesNotExist: ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST})
@transaction.atomic
def post(self, request, id):
license = License.objects.get(pk=id)
remove_all_users_from_license(request.user, license)
return Response(status=204)
class AdminLicenseLookupUsersView(APIView):
permission_classes = (IsAdminUser,)
@extend_schema(
tags=["Admin"],
parameters=[
OpenApiParameter(
name="id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The internal identifier of the license, this is `id` and "
"not `license_id`.",
),
OpenApiParameter(
name="search",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
description="If provided, only users where the name or email "
"contains the value are returned.",
),
OpenApiParameter(
name="page",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT,
description="Defines which page of users should be returned.",
),
OpenApiParameter(
name="size",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT,
description=f"Defines how many users should be returned per " f"page.",
),
],
operation_id="admin_premium_license_lookup_users",
description=(
"This endpoint can be used to lookup users that must be added to a "
"premium license. Users that are already in the license are not returned "
"here. Optionally a `search` query parameter can be provided to filter "
"the results."
),
responses={
200: get_example_pagination_serializer_class(
PremiumLicenseUserLookupSerializer
),
404: get_error_schema(["ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"]),
},
)
@map_exceptions({License.DoesNotExist: ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST})
def get(self, request, id):
license = License.objects.get(pk=id)
search = request.GET.get("search")
queryset = User.objects.filter(
~Q(id__in=license.users.all().values_list("user_id", flat=True))
).order_by("first_name")
if search:
queryset = queryset.filter(
Q(first_name__icontains=search) | Q(email__icontains=search)
)
paginator = PageNumberPagination(limit_page_size=settings.ROW_PAGE_SIZE_LIMIT)
page = paginator.paginate_queryset(queryset, request, self)
serializer = PremiumLicenseUserLookupSerializer(page, many=True)
return paginator.get_paginated_response(serializer.data)
class AdminCheckLicense(APIView):
permission_classes = (IsAdminUser,)
@extend_schema(
tags=["Admin"],
parameters=[
OpenApiParameter(
name="id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The internal identifier of the license, this is `id` and "
"not `license_id`.",
),
],
operation_id="admin_premium_license_check",
description=(
"This endpoint checks with the authority if the license needs to be "
"updated. It also checks if the license is operating within its limits "
"and might take action on that. It could also happen that the license has "
"been deleted because there is an instance id mismatch or because it's "
"invalid. In that case a `204` status code is returned."
),
responses={
200: PremiumLicenseWithUsersSerializer,
404: get_error_schema(["ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"]),
},
)
@map_exceptions({License.DoesNotExist: ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST})
def get(self, request, id):
license_object = License.objects.get(pk=id)
updated_licenses = check_licenses([license_object])
if not updated_licenses[0].pk:
# If the primary key is None, it means that the license has been deleted
# which could happen when checking the license. In that case, we want to
# respond with a 204 (empty response) respond to indicate that it was
# deleted.
return Response(status=204)
else:
return Response(PremiumLicenseWithUsersSerializer(updated_licenses[0]).data)

View file

@ -1,4 +1,5 @@
from django.conf import settings
from django.db import transaction
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, OpenApiParameter
from rest_framework.pagination import LimitOffsetPagination
@ -16,6 +17,8 @@ from baserow.contrib.database.rows.exceptions import RowDoesNotExist
from baserow.contrib.database.table.exceptions import TableDoesNotExist
from baserow.core.exceptions import UserNotInGroup
from baserow_premium.row_comments.handler import RowCommentHandler
from baserow_premium.license.handler import check_active_premium_license
from .serializers import RowCommentSerializer, RowCommentCreateSerializer
@ -85,6 +88,8 @@ class RowCommentView(APIView):
}
)
def get(self, request, table_id, row_id):
check_active_premium_license(request.user)
comments = RowCommentHandler.get_comments(request.user, table_id, row_id)
if LimitOffsetPagination.limit_query_param in request.GET:
@ -139,7 +144,10 @@ class RowCommentView(APIView):
}
)
@validate_body(RowCommentCreateSerializer)
@transaction.atomic
def post(self, request, table_id, row_id, data):
check_active_premium_license(request.user)
new_row_comment = RowCommentHandler.create_comment(
request.user, table_id, row_id, data["comment"]
)

View file

@ -1,11 +1,14 @@
from django.urls import path, include
from .license import urls as license_urls
from .admin import urls as admin_urls
from .row_comments import urls as row_comments_urls
app_name = "baserow_premium.api"
urlpatterns = [
path("licenses/", include(license_urls, namespace="license")),
path("admin/", include(admin_urls, namespace="admin")),
path("row_comments/", include(row_comments_urls, namespace="row_comments")),
]

View file

@ -0,0 +1,15 @@
from baserow.api.user.registries import UserDataType
from baserow_premium.license.handler import has_active_premium_license
class PremiumUserDataType(UserDataType):
type = "premium"
def get_user_data(self, user, request) -> dict:
"""
Someone who authenticates via the API should know beforehand if the related
user has a valid license for the premioum version.
"""
return {"valid_license": has_active_premium_license(user)}

View file

@ -6,12 +6,14 @@ class BaserowPremiumConfig(AppConfig):
def ready(self):
from baserow.core.registries import plugin_registry
from baserow.api.user.registries import user_data_registry
from baserow.contrib.database.export.registries import table_exporter_registry
from baserow.contrib.database.rows.registries import row_metadata_registry
from baserow_premium.row_comments.row_metadata_types import (
RowCommentCountMetadataType,
)
from baserow_premium.api.user.user_data_types import PremiumUserDataType
# noinspection PyUnresolvedReferences
import baserow_premium.row_comments.recievers # noqa: F401
@ -26,6 +28,8 @@ class BaserowPremiumConfig(AppConfig):
row_metadata_registry.register(RowCommentCountMetadataType())
user_data_registry.register(PremiumUserDataType())
# The signals must always be imported last because they use the registries
# which need to be filled first.
import baserow_premium.ws.signals # noqa: F403, F401

View file

@ -12,9 +12,21 @@ from baserow.contrib.database.export.file_writer import (
from baserow.contrib.database.export.registries import TableExporter
from baserow.contrib.database.views.view_types import GridViewType
from baserow_premium.license.handler import check_active_premium_license
from .utils import get_unique_name, safe_xml_tag_name, to_xml
class PremiumTableExporter(TableExporter):
def before_job_create(self, user, table, view, export_options):
"""
Checks if the related user access to a valid license before the job is created.
"""
check_active_premium_license(user)
super().before_job_create(user, table, view, export_options)
class JSONQuerysetSerializer(QuerysetSerializer):
def write_to_file(self, file_writer: FileWriter, export_charset="utf-8"):
"""
@ -49,7 +61,7 @@ class JSONQuerysetSerializer(QuerysetSerializer):
file_writer.write("\n]\n", encoding=export_charset)
class JSONTableExporter(TableExporter):
class JSONTableExporter(PremiumTableExporter):
type = "json"
@property
@ -112,7 +124,7 @@ class XMLQuerysetSerializer(QuerysetSerializer):
file_writer.write("</rows>\n", encoding=export_charset)
class XMLTableExporter(TableExporter):
class XMLTableExporter(PremiumTableExporter):
type = "xml"
@property

View file

@ -0,0 +1,5 @@
AUTHORITY_RESPONSE_OK = "ok"
AUTHORITY_RESPONSE_UPDATE = "update"
AUTHORITY_RESPONSE_DOES_NOT_EXIST = "does_not_exist"
AUTHORITY_RESPONSE_INSTANCE_ID_MISMATCH = "instance_id_mismatch"
AUTHORITY_RESPONSE_INVALID = "invalid"

View file

@ -0,0 +1,61 @@
from rest_framework.status import HTTP_402_PAYMENT_REQUIRED
from rest_framework.exceptions import APIException
class NoPremiumLicenseError(APIException):
"""
Raised when the related user does not have an active license for the premium
version.
"""
def __init__(self):
super().__init__(
{
"error": "ERROR_NO_ACTIVE_PREMIUM_LICENSE",
"detail": "The related user does not have access to the premium "
"version.",
},
code=HTTP_402_PAYMENT_REQUIRED,
)
self.status_code = HTTP_402_PAYMENT_REQUIRED
class InvalidPremiumLicenseError(Exception):
"""
Raised when a provided premium license is not valid. This could be because the
signature is incorrect or the payload does not contain the required information.
"""
class UnsupportedPremiumLicenseError(Exception):
"""
Raised when the version of the license is not supported. This probably means that
Baserow must be upgraded.
"""
class PremiumLicenseInstanceIdMismatchError(Exception):
"""
Raised when trying to register a license and the instance ids of the license and
self hosted copy don't match.
"""
class PremiumLicenseAlreadyExists(Exception):
"""Raised when trying to register a license that already exists."""
class PremiumLicenseHasExpired(Exception):
"""Raised when trying to register a license that is expired."""
class UserAlreadyOnPremiumLicenseError(Exception):
"""Raised when the user already has a seat in the license."""
class NoSeatsLeftInPremiumLicenseError(Exception):
"""Raised when there are no seats left in the license."""
class LicenseAuthorityUnavailable(Exception):
"""Raised when the license authority can't be reached."""

View file

@ -0,0 +1,542 @@
import base64
import binascii
import hashlib
import json
import logging
from typing import Union, List
from os.path import dirname, join
from dateutil import parser
import requests
from requests.exceptions import RequestException
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User as DjangoUser
from django.utils.timezone import now, make_aware, utc
from django.db import transaction
from django.db.models import Q
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from rest_framework.status import HTTP_200_OK
from baserow.core.exceptions import IsNotAdminError
from baserow.core.handler import CoreHandler
from baserow.ws.signals import broadcast_to_users
from .models import License, LicenseUser
from .exceptions import (
NoPremiumLicenseError,
InvalidPremiumLicenseError,
UnsupportedPremiumLicenseError,
PremiumLicenseInstanceIdMismatchError,
PremiumLicenseAlreadyExists,
PremiumLicenseHasExpired,
UserAlreadyOnPremiumLicenseError,
NoSeatsLeftInPremiumLicenseError,
LicenseAuthorityUnavailable,
)
from .constants import (
AUTHORITY_RESPONSE_UPDATE,
AUTHORITY_RESPONSE_DOES_NOT_EXIST,
AUTHORITY_RESPONSE_INSTANCE_ID_MISMATCH,
AUTHORITY_RESPONSE_INVALID,
)
logger = logging.getLogger(__name__)
User = get_user_model()
def has_active_premium_license(user: DjangoUser) -> bool:
"""
Checks if the provided user has an active license.
:param user: The user for whom must be checked if it has an active license.
:return: True if the user has an active license to the version.
"""
available_licenses = License.objects.filter(users__user_id__in=[user.id]).distinct()
for available_license in available_licenses:
try:
if (
available_license.product_code == "premium"
and available_license.is_active
):
return True
except InvalidPremiumLicenseError:
pass
return False
def check_active_premium_license(user):
"""
Raises the `NoPremiumLicenseError` if the user does not have an active premium
license.
"""
if not has_active_premium_license(user):
raise NoPremiumLicenseError()
def get_public_key():
"""
Returns the public key instance that can be used to verify licenses. A different
key file is loaded when Baserow is in debug mode.
"""
import baserow_premium
file_name = "public_key_debug.pem" if settings.DEBUG else "public_key.pem"
public_key_path = join(dirname(baserow_premium.__file__), file_name)
with open(public_key_path, "rb") as key_file:
public_key = serialization.load_pem_public_key(
key_file.read(), backend=default_backend()
)
return public_key
def decode_license(license_payload: bytes) -> dict:
"""
Tries to decode the provided license and returns the payload if successful.
:param license_payload: The raw license that must be decoded.
:raises InvalidPremiumLicenseError: When the provided license is invalid. This
could for example be when the signature or payload is invalid.
:raises UnsupportedPremiumLicenseError: When the provided license payload is an
unsupported version. If this happens, you probably need to update your Baserow
installation.
:return: If successful, the decoded license payload is returned.
"""
try:
payload_base64, signature_base64 = license_payload.split(b".")
except ValueError:
raise InvalidPremiumLicenseError(
"The provided payload does not follow the expected format."
)
pre_hashed = hashlib.sha256(payload_base64).hexdigest().encode()
try:
signature = base64.urlsafe_b64decode(signature_base64)
except binascii.Error:
raise InvalidPremiumLicenseError("Invalid base64 signature provided.")
public_key = get_public_key()
try:
public_key.verify(
signature,
pre_hashed,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256(),
)
except InvalidSignature:
raise InvalidPremiumLicenseError(
"The signature of the premium license is invalid."
)
try:
payload_json = base64.urlsafe_b64decode(payload_base64)
except binascii.Error:
raise InvalidPremiumLicenseError("Invalid base64 payload provided.")
try:
payload = json.loads(payload_json)
except json.decoder.JSONDecodeError:
raise InvalidPremiumLicenseError("Invalid JSON payload provided.")
if "version" not in payload:
raise InvalidPremiumLicenseError("The payload does not contain a version.")
if payload["version"] != 1:
raise UnsupportedPremiumLicenseError(
"Only license version 1 is supported. You probably need to update your "
"copy of Baserow."
)
return payload
def fetch_license_status_with_authority(license_payloads: List[Union[str, bytes]]):
"""
Fetches the state of the license with the authority. It could be that the license
must be updated because it has changed, it might have been deleted, the instance_id
might not match anymore or it might be invalid.
:param license_payloads: A list of licenses that must be checked with the authority.
:return: The state of each license provided.
"""
license_payloads = [
payload if isinstance(payload, str) else payload.decode()
for payload in license_payloads
]
settings_object = CoreHandler().get_settings()
try:
base_url = (
"http://172.17.0.1:8001" if settings.DEBUG else "https://api.baserow.io"
)
response = requests.post(
f"{base_url}/api/saas/licenses/check/",
json={
"licenses": license_payloads,
"instance_id": settings_object.instance_id,
},
timeout=10,
)
if response.status_code == HTTP_200_OK:
return response.json()
else:
raise LicenseAuthorityUnavailable(
"The license authority can't be reached because it didn't returned "
"with an ok response."
)
except RequestException:
raise LicenseAuthorityUnavailable(
"The license authority can't be reached because of a network error."
)
except json.decoder.JSONDecodeError:
raise LicenseAuthorityUnavailable(
"The license authority did not respond with valid json."
)
def check_licenses(license_objects: List[License]) -> List[License]:
"""
Checks the state of the licenses with the authority and checks if the licenses
are operating within their limits.
- It will update the license payload if needed.
- It removes the license if it doesn't exist, if it's invalid or if the instance
id doesn't match.
- It also checks if the license is operating within its limit. For example if the
license has not crossed the maximum amount of seats.
:param license_objects: The license objects that must be checked
:return: The updated license objects.
"""
licenses_to_check = [license_object.license for license_object in license_objects]
try:
authority_response = fetch_license_status_with_authority(licenses_to_check)
for license_object in license_objects:
if license_object.license not in authority_response:
continue
authority_check = authority_response[license_object.license]
if authority_check["type"] == AUTHORITY_RESPONSE_UPDATE:
license_object.license = authority_check["new_license_payload"]
license_object.save()
elif authority_check["type"] in [
AUTHORITY_RESPONSE_DOES_NOT_EXIST,
AUTHORITY_RESPONSE_INSTANCE_ID_MISMATCH,
AUTHORITY_RESPONSE_INVALID,
]:
license_object.delete()
except LicenseAuthorityUnavailable as e:
# If the license authority is unavailable for whatever reason, we don't want
# the check to fail because the self hosted instance might not have
# internet and the license is already validated locally.
logger.warning(str(e))
for license_object in license_objects:
# If the license object has been deleted we can skip it.
if not license_object.pk:
continue
# If the license payload could not be decoded, it must be deleted.
try:
license_object.payload
except InvalidPremiumLicenseError:
license_object.delete()
continue
seats_taken = license_object.users.all().count()
if seats_taken > license_object.seats:
# If there are more seats taken than the license allows, we need to
# remove the active seats that are outside of the limit.
LicenseUser.objects.filter(
pk__in=license_object.users.all()
.order_by("pk")
.values_list("pk")[license_object.seats : seats_taken]
).delete()
license_object.last_check = now()
license_object.save()
return license_objects
def register_license(
requesting_user: User, license_payload: Union[bytes, str]
) -> License:
"""
Registers a new license by adding it to the database. If a license with same id
already exists and the provided one was issued later, then the existing one will
be updated.
:param requesting_user: The user on whose behalf the license is registered.
:param license_payload: The license that must be decoded and added.
:raises PremiumLicenseAlreadyExists: When the license already exists.
:raises PremiumLicenseHasExpired: When the license has expired.
:return: The created license instance.
"""
if not requesting_user.is_staff:
raise IsNotAdminError()
if isinstance(license_payload, str):
license_payload_as_string = license_payload
license_payload = license_payload.encode()
else:
license_payload_as_string = license_payload.decode()
try:
authority_response = fetch_license_status_with_authority(
[license_payload_as_string]
)
authority_check = authority_response[license_payload_as_string]
if authority_check["type"] == AUTHORITY_RESPONSE_UPDATE:
# If there is a newer version of the license, we can replace the license
# payload that we have in memory with that one.
license_payload_as_string = authority_check["new_license_payload"]
license_payload = license_payload_as_string.encode()
elif authority_check["type"] == AUTHORITY_RESPONSE_DOES_NOT_EXIST:
# If the authority tells us that the license does not exist there,
# we must stop the registering.
raise InvalidPremiumLicenseError(
"The license does not exist according to the authority."
)
elif authority_check["type"] == AUTHORITY_RESPONSE_INSTANCE_ID_MISMATCH:
# If the authority tells us the instance id doesn't match,
# we can immediately raise that error.
raise PremiumLicenseInstanceIdMismatchError(
"The instance id doesn't match according to the authority."
)
elif authority_check["type"] == AUTHORITY_RESPONSE_INVALID:
raise InvalidPremiumLicenseError(
"The license is invalid according to the authority."
)
except LicenseAuthorityUnavailable as e:
# If the license authority is unavailable for whatever reason, we don't want
# the registering to fail because the self hosted instance might not have
# internet and the license can be validated locally with the public key.
logger.warning(str(e))
# Try to decode the provided license payload in order to trigger the errors if
# needed. We also need the `valid_through` date to check if the license has expired.
decoded_license_payload = decode_license(license_payload)
valid_through = make_aware(
parser.parse(decoded_license_payload["valid_through"]), utc
)
issued_on = make_aware(parser.parse(decoded_license_payload["issued_on"]), utc)
if valid_through < now():
raise PremiumLicenseHasExpired(
"Cannot add the license because it has already expired."
)
# The `instance_id` of the license must match with the `instance_id` of the self
# hosted copy.
settings_object = CoreHandler().get_settings()
if decoded_license_payload["instance_id"] != settings_object.instance_id:
raise PremiumLicenseInstanceIdMismatchError(
"The license instance id does not match the instance id."
)
# Loop over all licenses to check if a license with the same ID already exists. We
# can't use `objects.filter` because we need to decode the license with the
# public key before we can extract the id.
for license_object in License.objects.all():
if license_object.license_id == decoded_license_payload["id"]:
# If the `issued_on` date of the existing license is lower then the new
# license, we want to update it because a new one has been issued later
# and is newer.
if license_object.issued_on < issued_on:
license_object.license = license_payload_as_string
license_object.save()
return license_object
# If the `issued_on` date of the existing license is higher or equal to
# the new license, we want to raise the exception that the most license
# already exists.
else:
raise PremiumLicenseAlreadyExists("The license already exists.")
# If the license doesn't exist we want to create a new one.
return License.objects.create(license=license_payload_as_string)
def remove_license(requesting_user: User, license: License):
"""
Removes an existing license. If the license is still active, all the users that
are on that license will lose access to the premium features.
:param requesting_user: The user on whose behalf the license is removed.
:param license: The license that must be removed.
"""
if not requesting_user.is_staff:
raise IsNotAdminError()
license.delete()
def add_user_to_license(
requesting_user: User, license_object: License, user: User
) -> LicenseUser:
"""
Adds a user to the provided license.
:param requesting_user: The user on whose behalf the user is added to the license.
:param license_object: The license that the user must be added to.
:param user: The user that must be added to the license.
:raises UserAlreadyInPremiumLicenseError: When the user already has a seat in the
license.
:raises NoSeatsLeftInPremiumLicenseError: When the license doesn't have any seats
left.
:return: The newly created license user object.
"""
if not requesting_user.is_staff:
raise IsNotAdminError()
if LicenseUser.objects.filter(license=license_object, user=user).exists():
raise UserAlreadyOnPremiumLicenseError(
"The user already has a seat on this license."
)
seats_taken = license_object.users.all().count()
if seats_taken >= license_object.seats:
raise NoSeatsLeftInPremiumLicenseError(
"There aren't any seats left in the license."
)
if license_object.is_active:
transaction.on_commit(
lambda: broadcast_to_users.delay(
[user.id],
{
"type": "user_data_updated",
"user_data": {"premium": {"valid_license": True}},
},
)
)
return LicenseUser.objects.create(license=license_object, user=user)
def remove_user_from_license(
requesting_user: User, license_object: License, user: User
):
"""
Removes the provided user from the provided license if the user has a seat.
:param requesting_user: The user on whose behalf the user is removed from the
license.
:param license_object: The license object where the user must be removed from.
:param user: The user that must be removed from the license.
"""
if not requesting_user.is_staff:
raise IsNotAdminError()
LicenseUser.objects.filter(license=license_object, user=user).delete()
if license_object.is_active:
transaction.on_commit(
lambda: broadcast_to_users.delay(
[user.id],
{
"type": "user_data_updated",
"user_data": {"premium": {"valid_license": False}},
},
)
)
def fill_remaining_seats_of_license(
requesting_user: User, license_object: License
) -> List[LicenseUser]:
"""
Fills the remaining seats of the license with additional users.
:param requesting_user: The user on whose behalf the request is made.
:param license_object: The license object where the users must be added to.
:return: A list of created license users.
"""
if not requesting_user.is_staff:
raise IsNotAdminError()
already_in_license = license_object.users.all().values_list("user_id", flat=True)
remaining_seats = license_object.seats - len(already_in_license)
if remaining_seats > 0:
users_to_add = User.objects.filter(~Q(id__in=already_in_license)).order_by(
"id"
)[:remaining_seats]
user_licenses = [
LicenseUser(license=license_object, user=user) for user in users_to_add
]
LicenseUser.objects.bulk_create(user_licenses)
if license_object.is_active:
transaction.on_commit(
lambda: broadcast_to_users.delay(
[user_license.user_id for user_license in user_licenses],
{
"type": "user_data_updated",
"user_data": {"premium": {"valid_license": True}},
},
)
)
return user_licenses
return []
def remove_all_users_from_license(requesting_user: User, license_object: License):
"""
Removed all the users from a license. This will clear up all the seats.
:param requesting_user: The user on whose behalf the users are removed.
:param license_object: The license object where the users must be removed from.
"""
if not requesting_user.is_staff:
raise IsNotAdminError()
license_users = LicenseUser.objects.filter(license=license_object)
license_user_ids = list(license_users.values_list("user_id", flat=True))
license_users.delete()
if license_object.is_active:
transaction.on_commit(
lambda: broadcast_to_users.delay(
license_user_ids,
{
"type": "user_data_updated",
"user_data": {"premium": {"valid_license": False}},
},
)
)

View file

@ -0,0 +1,74 @@
from dateutil import parser
from django.db import models
from django.contrib.auth import get_user_model
from django.utils.timezone import utc, now, make_aware
from django.utils.functional import cached_property
User = get_user_model()
class License(models.Model):
license = models.TextField()
last_check = models.DateTimeField(null=True)
def save(self, *args, **kwargs):
try:
del self.payload
except AttributeError:
pass
return super().save(*args, **kwargs)
@cached_property
def payload(self):
from .handler import decode_license
premium_license = self.license
if isinstance(premium_license, str):
premium_license = premium_license.encode()
return decode_license(premium_license)
@property
def license_id(self):
return self.payload["id"]
@property
def valid_from(self):
return make_aware(parser.parse(self.payload["valid_from"]), utc)
@property
def valid_through(self):
return make_aware(parser.parse(self.payload["valid_through"]), utc)
@property
def is_active(self):
return self.valid_from <= now() <= self.valid_through
@property
def product_code(self):
return self.payload["product_code"]
@property
def seats(self):
return self.payload["seats"]
@property
def issued_on(self):
return make_aware(parser.parse(self.payload["issued_on"]), utc)
@property
def issued_to_email(self):
return self.payload["issued_to_email"]
@property
def issued_to_name(self):
return self.payload["issued_to_name"]
class LicenseUser(models.Model):
license = models.ForeignKey(License, on_delete=models.CASCADE, related_name="users")
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
unique_together = ("license", "user")

View file

@ -0,0 +1,27 @@
from datetime import timedelta
from django.db import transaction
from baserow.config.celery import app
@app.task(bind=True, queue="export")
def license_check(self):
"""
Periodic tasks that check all the licenses with the authority.
"""
from .handler import check_licenses
from .models import License
all_licenses = License.objects.all()
if len(all_licenses) > 0:
with transaction.atomic():
check_licenses(all_licenses)
# noinspection PyUnusedLocal
@app.on_after_finalize.connect
def setup_periodic_tasks(sender, **kwargs):
sender.add_periodic_task(timedelta(hours=1), license_check.s())

View file

@ -0,0 +1,64 @@
# Generated by Django 3.2.6 on 2021-10-12 19:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("baserow_premium", "0001_row_comments"),
]
operations = [
migrations.CreateModel(
name="License",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("license", models.TextField()),
("last_check", models.DateTimeField(null=True)),
],
),
migrations.CreateModel(
name="LicenseUser",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"license",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="users",
to="baserow_premium.license",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("license", "user")},
},
),
]

View file

@ -0,0 +1,5 @@
from .license.models import License, LicenseUser
from .row_comments.models import RowComment
__all__ = ["License", "LicenseUser", "RowComment"]

View file

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArolpEK7fTTeaM0JiLb/G
eDGneylpJBoKcHpQi0FPqh1bgLagGMtylFYlzz8ZyabI81cJ9VN40WrgbwuMyk8q
OUoA9e26TcZ5N33Xjao/iksWrNbQf6Q/gwYM6uP1rJggzqH4Si1hmv96+OPRCuPG
4KltzmrGazI4HPi6vdZ2fPuFaqYbUo6Ygddk7Y9Grd3HUb80r5FanotesVe5xeta
GLT1UT8eIt/nfhFGd2LIVbgYG8gpa4ZnJo8IWr48+AMIVeFjZgdvOSaabSrvlTfb
Fu70047ZpUmghlSBLL/O9xyRO2fIrRsccV7pfgeOLV6qxcb2ASWnQRCaGprGot5u
DQIDAQAB
-----END PUBLIC KEY-----

View file

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu9/6ZS7rvJvxkCrJB+Y+
E2oGIOUVju3hyueB+Gs3Fy0GFtklt8IvB/qatgMyzYjxGUzQLHu8kkEUmNEztgQD
jny1Yy/kVjBRBRDps4kaw29z4QgGqiS83vTN2iQ0u5w3Ok23+VxGzbFrRLUyYuWW
pkMz0SC3FnuAZzpWg9pgyqpiS+s6gVCgcVOuN9+/MDNkCkaLmW59COp8hHPAVJgp
IhZTxzY7/ESNLBgVYwDoj7gyNHNuwiAxdAs6WpxN69YAEGgmMt9aEA75pdJzOWsD
YgooCo7Ptyl5BsSsIYYPW4ZFPdzElpj704dOO8flPDxhHNpnMEWoucZ2BQikJWmL
BQIDAQAB
-----END PUBLIC KEY-----

View file

@ -6,6 +6,7 @@ from baserow.contrib.database.table.handler import TableHandler
from baserow_premium.row_comments.exceptions import InvalidRowCommentException
from baserow_premium.row_comments.models import RowComment
from baserow_premium.row_comments.signals import row_comment_created
from baserow_premium.license.handler import check_active_premium_license
User = get_user_model()
@ -53,6 +54,8 @@ class RowCommentHandler:
:raises InvalidRowCommentException: If the comment is blank or None.
"""
check_active_premium_license(requesting_user)
if comment is None or comment == "":
raise InvalidRowCommentException()

View file

@ -0,0 +1,3 @@
from .license.tasks import license_check, setup_periodic_tasks
__all__ = ["license_check", "setup_periodic_tasks"]

View file

@ -3,31 +3,38 @@ from pytz import timezone
from datetime import timedelta, datetime, date
from django.test.utils import override_settings
from baserow.core.models import UserLogEntry
from baserow_premium.admin.dashboard.handler import AdminDashboardHandler
@pytest.mark.django_db
def test_get_new_user_counts(data_fixture):
@override_settings(DEBUG=True)
def test_get_new_user_counts(premium_data_fixture):
tz = timezone("UTC")
data_fixture.create_user(date_joined=datetime(2020, 12, 30, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 1, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 2, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 3, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 4, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 5, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 10, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 23, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 24, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 25, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 26, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 27, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 28, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 29, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 30, 12, 1, tzinfo=tz))
data_fixture.create_user(date_joined=datetime(2021, 1, 30, 15, 1, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2020, 12, 30, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 1, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 2, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 3, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 4, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 5, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 10, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 23, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 24, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 25, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 26, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 27, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 28, tzinfo=tz))
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 29, tzinfo=tz))
premium_data_fixture.create_user(
date_joined=datetime(2021, 1, 30, 12, 1, tzinfo=tz)
)
premium_data_fixture.create_user(
date_joined=datetime(2021, 1, 30, 15, 1, tzinfo=tz)
)
handler = AdminDashboardHandler()
now = datetime(2021, 1, 30, 23, 59, tzinfo=tz)
@ -67,12 +74,13 @@ def test_get_new_user_counts(data_fixture):
@pytest.mark.django_db
def test_get_active_user_counts(data_fixture):
@override_settings(DEBUG=True)
def test_get_active_user_counts(premium_data_fixture):
tz = timezone("UTC")
user_1 = data_fixture.create_user()
user_2 = data_fixture.create_user()
user_3 = data_fixture.create_user()
user_1 = premium_data_fixture.create_user()
user_2 = premium_data_fixture.create_user()
user_3 = premium_data_fixture.create_user()
def create_entries(user, dates):
for d in dates:
@ -184,18 +192,33 @@ def test_get_active_user_counts(data_fixture):
@pytest.mark.django_db
def test_get_new_users_per_day(data_fixture):
@override_settings(DEBUG=True)
def test_get_new_users_per_day(premium_data_fixture):
utc = timezone("UTC")
gmt3 = timezone("Etc/GMT+3")
data_fixture.create_user(date_joined=datetime(2020, 12, 29, 12, 1, tzinfo=utc))
data_fixture.create_user(date_joined=datetime(2021, 1, 1, 1, 1, tzinfo=utc))
data_fixture.create_user(date_joined=datetime(2021, 1, 1, 12, 1, tzinfo=utc))
data_fixture.create_user(date_joined=datetime(2021, 1, 2, 12, 1, tzinfo=utc))
data_fixture.create_user(date_joined=datetime(2021, 1, 2, 12, 1, tzinfo=utc))
data_fixture.create_user(date_joined=datetime(2021, 1, 2, 12, 1, tzinfo=utc))
data_fixture.create_user(date_joined=datetime(2021, 1, 30, 12, 1, tzinfo=utc))
data_fixture.create_user(date_joined=datetime(2021, 1, 30, 15, 1, tzinfo=utc))
premium_data_fixture.create_user(
date_joined=datetime(2020, 12, 29, 12, 1, tzinfo=utc)
)
premium_data_fixture.create_user(date_joined=datetime(2021, 1, 1, 1, 1, tzinfo=utc))
premium_data_fixture.create_user(
date_joined=datetime(2021, 1, 1, 12, 1, tzinfo=utc)
)
premium_data_fixture.create_user(
date_joined=datetime(2021, 1, 2, 12, 1, tzinfo=utc)
)
premium_data_fixture.create_user(
date_joined=datetime(2021, 1, 2, 12, 1, tzinfo=utc)
)
premium_data_fixture.create_user(
date_joined=datetime(2021, 1, 2, 12, 1, tzinfo=utc)
)
premium_data_fixture.create_user(
date_joined=datetime(2021, 1, 30, 12, 1, tzinfo=utc)
)
premium_data_fixture.create_user(
date_joined=datetime(2021, 1, 30, 15, 1, tzinfo=utc)
)
handler = AdminDashboardHandler()
@ -225,13 +248,14 @@ def test_get_new_users_per_day(data_fixture):
@pytest.mark.django_db
def test_get_active_users_per_day(data_fixture):
@override_settings(DEBUG=True)
def test_get_active_users_per_day(premium_data_fixture):
utc = timezone("UTC")
gmt3 = timezone("Etc/GMT+3")
user_1 = data_fixture.create_user()
user_2 = data_fixture.create_user()
user_3 = data_fixture.create_user()
user_1 = premium_data_fixture.create_user()
user_2 = premium_data_fixture.create_user()
user_3 = premium_data_fixture.create_user()
def create_entries(user, dates):
for d in dates:

View file

@ -2,6 +2,7 @@ from unittest.mock import patch
import pytest
from django.db import connection
from django.test.utils import override_settings
from baserow.contrib.database.models import Database, Table
from baserow.core.exceptions import IsNotAdminError
@ -11,24 +12,30 @@ from baserow.core.models import (
)
from baserow_premium.admin.groups.exceptions import CannotDeleteATemplateGroupError
from baserow_premium.admin.groups.handler import GroupsAdminHandler
from baserow_premium.license.exceptions import NoPremiumLicenseError
@pytest.mark.django_db
@override_settings(DEBUG=True)
@patch("baserow.core.signals.group_deleted.send")
def test_delete_group(send_mock, data_fixture):
staff_user = data_fixture.create_user(is_staff=True)
normal_user = data_fixture.create_user(is_staff=False)
other_user = data_fixture.create_user()
group_1 = data_fixture.create_group(user=other_user)
database = data_fixture.create_database_application(group=group_1)
table = data_fixture.create_database_table(database=database)
def test_delete_group(send_mock, premium_data_fixture):
staff_user_with_premium_license = premium_data_fixture.create_user(
is_staff=True, has_active_premium_license=True
)
normal_user = premium_data_fixture.create_user(
is_staff=False, has_active_premium_license=True
)
other_user = premium_data_fixture.create_user()
group_1 = premium_data_fixture.create_group(user=other_user)
database = premium_data_fixture.create_database_application(group=group_1)
table = premium_data_fixture.create_database_table(database=database)
handler = GroupsAdminHandler()
with pytest.raises(IsNotAdminError):
handler.delete_group(normal_user, group_1)
handler.delete_group(staff_user, group_1)
handler.delete_group(staff_user_with_premium_license, group_1)
send_mock.assert_called_once()
assert send_mock.call_args[1]["group"].id == group_1.id
@ -44,14 +51,17 @@ def test_delete_group(send_mock, data_fixture):
@pytest.mark.django_db
@override_settings(DEBUG=True)
@patch("baserow.core.signals.group_deleted.send")
def test_cant_delete_template_group(send_mock, data_fixture):
staff_user = data_fixture.create_user(is_staff=True)
group_1 = data_fixture.create_group(user=staff_user)
database = data_fixture.create_database_application(group=group_1)
data_fixture.create_database_table(database=database)
def test_cant_delete_template_group(send_mock, premium_data_fixture):
staff_user = premium_data_fixture.create_user(
is_staff=True, has_active_premium_license=True
)
group_1 = premium_data_fixture.create_group(user=staff_user)
database = premium_data_fixture.create_database_application(group=group_1)
premium_data_fixture.create_database_table(database=database)
data_fixture.create_template(group=group_1)
premium_data_fixture.create_template(group=group_1)
handler = GroupsAdminHandler()
@ -60,3 +70,20 @@ def test_cant_delete_template_group(send_mock, data_fixture):
send_mock.assert_not_called()
assert Group.objects.all().count() == 1
@pytest.mark.django_db
@override_settings(DEBUG=True)
@patch("baserow.core.signals.group_deleted.send")
def test_delete_group_without_premium_license(send_mock, premium_data_fixture):
staff_user = premium_data_fixture.create_user(is_staff=True)
other_user = premium_data_fixture.create_user()
group_1 = premium_data_fixture.create_group(user=other_user)
handler = GroupsAdminHandler()
with pytest.raises(NoPremiumLicenseError):
handler.delete_group(staff_user, group_1)
send_mock.assert_not_called()
assert Group.objects.all().count() == 1

View file

@ -1,5 +1,6 @@
import pytest
from django.contrib.auth import get_user_model
from django.test.utils import override_settings
from baserow.core.exceptions import IsNotAdminError
from baserow.core.user.exceptions import PasswordDoesNotMatchValidation
@ -11,6 +12,7 @@ from baserow_premium.admin.users.exceptions import (
from baserow_premium.admin.users.handler import (
UserAdminHandler,
)
from baserow_premium.license.exceptions import NoPremiumLicenseError
User = get_user_model()
invalid_passwords = [
@ -32,52 +34,74 @@ invalid_passwords = [
@pytest.mark.django_db
def test_admin_can_delete_user(data_fixture):
@override_settings(DEBUG=True)
def test_admin_can_delete_user(premium_data_fixture):
handler = UserAdminHandler()
admin_user = data_fixture.create_user(
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
user_to_delete = data_fixture.create_user(
user_to_delete = premium_data_fixture.create_user(
email="delete_me@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
handler.delete_user(admin_user, user_to_delete.id)
assert not User.objects.filter(id=user_to_delete.id).exists()
@pytest.mark.django_db
def test_non_admin_cant_delete_user(data_fixture):
@override_settings(DEBUG=True)
def test_non_admin_cant_delete_user(premium_data_fixture):
handler = UserAdminHandler()
non_admin_user = data_fixture.create_user(
non_admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=False,
has_active_premium_license=True,
)
with pytest.raises(IsNotAdminError):
handler.delete_user(non_admin_user, non_admin_user.id)
@pytest.mark.django_db
def test_admin_can_modify_allowed_user_attributes(data_fixture):
@override_settings(DEBUG=True)
def test_admin_delete_user_without_premium_license(premium_data_fixture):
handler = UserAdminHandler()
admin_user = data_fixture.create_user(
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
)
user_to_modify = data_fixture.create_user(
with pytest.raises(NoPremiumLicenseError):
handler.delete_user(admin_user, admin_user.id)
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_can_modify_allowed_user_attributes(premium_data_fixture):
handler = UserAdminHandler()
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
user_to_modify = premium_data_fixture.create_user(
email="delete_me@test.nl",
password="password",
first_name="Test1",
is_staff=False,
is_active=False,
has_active_premium_license=True,
)
old_password = user_to_modify.password
handler.update_user(
@ -101,25 +125,29 @@ def test_admin_can_modify_allowed_user_attributes(data_fixture):
@pytest.mark.django_db
def test_admin_can_deactive_and_unstaff_other_users(data_fixture):
@override_settings(DEBUG=True)
def test_admin_can_deactive_and_unstaff_other_users(premium_data_fixture):
handler = UserAdminHandler()
admin_user = data_fixture.create_user(
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
staff_user = data_fixture.create_user(
staff_user = premium_data_fixture.create_user(
email="staff@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
active_user = data_fixture.create_user(
active_user = premium_data_fixture.create_user(
email="active@test.nl",
password="password",
first_name="Test1",
is_active=True,
has_active_premium_license=True,
)
handler.update_user(
@ -140,22 +168,25 @@ def test_admin_can_deactive_and_unstaff_other_users(data_fixture):
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_updating_a_users_password_uses_djangos_built_in_smart_set_password(
data_fixture, mocker
premium_data_fixture, mocker
):
handler = UserAdminHandler()
admin_user = data_fixture.create_user(
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
user_to_modify = data_fixture.create_user(
user_to_modify = premium_data_fixture.create_user(
email="delete_me@test.nl",
password="password",
first_name="Test1",
is_staff=False,
is_active=False,
has_active_premium_license=True,
)
old_password_hash = user_to_modify.password
set_password_spy = mocker.spy(User, "set_password")
@ -170,25 +201,28 @@ def test_updating_a_users_password_uses_djangos_built_in_smart_set_password(
@pytest.mark.django_db
@override_settings(DEBUG=True)
@pytest.mark.parametrize("invalid_password", invalid_passwords)
def test_updating_a_users_password_with_invalid_password_raises_error(
data_fixture, invalid_password
premium_data_fixture, invalid_password
):
handler = UserAdminHandler()
valid_password = "thisIsAValidPassword"
admin_user = data_fixture.create_user(
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password=valid_password,
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
user_to_modify = data_fixture.create_user(
user_to_modify = premium_data_fixture.create_user(
email="delete_me@test.nl",
password=valid_password,
first_name="Test1",
is_staff=False,
is_active=False,
has_active_premium_license=True,
)
with pytest.raises(PasswordDoesNotMatchValidation):
@ -202,13 +236,15 @@ def test_updating_a_users_password_with_invalid_password_raises_error(
@pytest.mark.django_db
def test_non_admin_cant_edit_user(data_fixture):
@override_settings(DEBUG=True)
def test_non_admin_cant_edit_user(premium_data_fixture):
handler = UserAdminHandler()
non_admin_user = data_fixture.create_user(
non_admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=False,
has_active_premium_license=True,
)
with pytest.raises(IsNotAdminError):
handler.update_user(non_admin_user, non_admin_user.id, "new_email@example.com")
@ -217,14 +253,16 @@ def test_non_admin_cant_edit_user(data_fixture):
@pytest.mark.django_db
def test_admin_cant_deactivate_themselves(data_fixture):
@override_settings(DEBUG=True)
def test_admin_cant_deactivate_themselves(premium_data_fixture):
handler = UserAdminHandler()
admin_user = data_fixture.create_user(
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
is_active=True,
has_active_premium_license=True,
)
with pytest.raises(CannotDeactivateYourselfException):
handler.update_user(
@ -237,14 +275,16 @@ def test_admin_cant_deactivate_themselves(data_fixture):
@pytest.mark.django_db
def test_admin_cant_destaff_themselves(data_fixture):
@override_settings(DEBUG=True)
def test_admin_cant_destaff_themselves(premium_data_fixture):
handler = UserAdminHandler()
admin_user = data_fixture.create_user(
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
is_active=True,
has_active_premium_license=True,
)
with pytest.raises(CannotDeactivateYourselfException):
handler.update_user(
@ -257,14 +297,30 @@ def test_admin_cant_destaff_themselves(data_fixture):
@pytest.mark.django_db
def test_admin_cant_delete_themselves(data_fixture):
@override_settings(DEBUG=True)
def test_admin_update_user_without_premium_license(premium_data_fixture):
handler = UserAdminHandler()
admin_user = data_fixture.create_user(
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
)
with pytest.raises(NoPremiumLicenseError):
handler.update_user(admin_user, admin_user.id)
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_cant_delete_themselves(premium_data_fixture):
handler = UserAdminHandler()
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
is_active=True,
has_active_premium_license=True,
)
with pytest.raises(CannotDeleteYourselfException):
handler.delete_user(admin_user, admin_user.id)
@ -273,28 +329,32 @@ def test_admin_cant_delete_themselves(data_fixture):
@pytest.mark.django_db
def test_raises_exception_when_deleting_an_unknown_user(data_fixture):
@override_settings(DEBUG=True)
def test_raises_exception_when_deleting_an_unknown_user(premium_data_fixture):
handler = UserAdminHandler()
admin_user = data_fixture.create_user(
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
is_active=True,
has_active_premium_license=True,
)
with pytest.raises(UserDoesNotExistException):
handler.delete_user(admin_user, 99999)
@pytest.mark.django_db
def test_raises_exception_when_updating_an_unknown_user(data_fixture):
@override_settings(DEBUG=True)
def test_raises_exception_when_updating_an_unknown_user(premium_data_fixture):
handler = UserAdminHandler()
admin_user = data_fixture.create_user(
admin_user = premium_data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
is_active=True,
has_active_premium_license=True,
)
with pytest.raises(UserDoesNotExistException):
handler.update_user(admin_user, 99999, username="new_password")

View file

@ -2,9 +2,11 @@ import pytest
from freezegun import freeze_time
from django.shortcuts import reverse
from django.test.utils import override_settings
from rest_framework.status import (
HTTP_200_OK,
HTTP_402_PAYMENT_REQUIRED,
HTTP_403_FORBIDDEN,
)
@ -12,16 +14,17 @@ from baserow.core.models import UserLogEntry
@pytest.mark.django_db
def test_admin_dashboard(api_client, data_fixture):
@override_settings(DEBUG=True)
def test_admin_dashboard(api_client, premium_data_fixture):
with freeze_time("2020-01-01 00:01"):
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False,
normal_user, normal_token = premium_data_fixture.create_user_and_token(
is_staff=False, has_active_premium_license=True
)
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
admin_user, admin_token = premium_data_fixture.create_user_and_token(
is_staff=True, has_active_premium_license=True
)
data_fixture.create_database_application(user=normal_user)
premium_data_fixture.create_database_application(user=normal_user)
UserLogEntry.objects.create(actor=admin_user, action="SIGNED_IN")
response = api_client.get(
@ -83,3 +86,13 @@ def test_admin_dashboard(api_client, data_fixture):
"new_users_per_day": [{"date": "2019-12-31", "count": 2}],
"active_users_per_day": [{"date": "2019-12-31", "count": 1}],
}
premium_data_fixture.remove_all_active_premium_licenses(admin_user)
url = reverse("api:premium:admin:dashboard:dashboard")
response = api_client.get(
url,
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
assert response.json()["error"] == "ERROR_NO_ACTIVE_PREMIUM_LICENSE"

View file

@ -2,9 +2,11 @@ import pytest
from django.utils.timezone import make_aware, datetime, utc
from django.shortcuts import reverse
from django.test.utils import override_settings
from rest_framework.status import (
HTTP_200_OK,
HTTP_402_PAYMENT_REQUIRED,
HTTP_403_FORBIDDEN,
HTTP_404_NOT_FOUND,
HTTP_400_BAD_REQUEST,
@ -14,7 +16,8 @@ from baserow.core.models import Group
@pytest.mark.django_db
def test_list_admin_groups(api_client, data_fixture, django_assert_num_queries):
@override_settings(DEBUG=True)
def test_list_admin_groups(api_client, premium_data_fixture, django_assert_num_queries):
"""
This endpoint doesn't need to be tested extensively because it uses the same base
class as the list users endpoint which already has extensive tests. We only need to
@ -22,22 +25,30 @@ def test_list_admin_groups(api_client, data_fixture, django_assert_num_queries):
"""
created = make_aware(datetime(2020, 4, 10, 0, 0, 0), utc)
staff_user, staff_token = data_fixture.create_user_and_token(is_staff=True)
normal_user, normal_token = data_fixture.create_user_and_token()
group_1 = data_fixture.create_group(name="A")
staff_user, staff_token = premium_data_fixture.create_user_and_token(
is_staff=True, has_active_premium_license=True
)
normal_user, normal_token = premium_data_fixture.create_user_and_token(
has_active_premium_license=True
)
group_1 = premium_data_fixture.create_group(name="A")
group_1.created_on = created
group_1.save()
group_2 = data_fixture.create_group(name="B", created_on=created)
group_2 = premium_data_fixture.create_group(name="B", created_on=created)
group_2.created_on = created
group_2.save()
template_group = data_fixture.create_group(name="Template", created_on=created)
data_fixture.create_template(group=template_group)
data_fixture.create_user_group(
template_group = premium_data_fixture.create_group(
name="Template", created_on=created
)
premium_data_fixture.create_template(group=template_group)
premium_data_fixture.create_user_group(
group=group_1, user=normal_user, permissions="MEMBER"
)
data_fixture.create_user_group(group=group_2, user=normal_user, permissions="ADMIN")
data_fixture.create_database_application(group=group_1)
data_fixture.create_database_application(group=group_1)
premium_data_fixture.create_user_group(
group=group_2, user=normal_user, permissions="ADMIN"
)
premium_data_fixture.create_database_application(group=group_1)
premium_data_fixture.create_database_application(group=group_1)
response = api_client.get(
reverse("api:premium:admin:groups:list"),
@ -46,7 +57,7 @@ def test_list_admin_groups(api_client, data_fixture, django_assert_num_queries):
)
assert response.status_code == HTTP_403_FORBIDDEN
with django_assert_num_queries(5):
with django_assert_num_queries(6):
response = api_client.get(
reverse("api:premium:admin:groups:list"),
format="json",
@ -158,12 +169,26 @@ def test_list_admin_groups(api_client, data_fixture, django_assert_num_queries):
],
}
premium_data_fixture.remove_all_active_premium_licenses(staff_user)
response = api_client.get(
f'{reverse("api:premium:admin:groups:list")}',
format="json",
HTTP_AUTHORIZATION=f"JWT {staff_token}",
)
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
assert response.json()["error"] == "ERROR_NO_ACTIVE_PREMIUM_LICENSE"
@pytest.mark.django_db
def test_delete_group(api_client, data_fixture):
normal_user, normal_token = data_fixture.create_user_and_token()
staff_user, staff_token = data_fixture.create_user_and_token(is_staff=True)
group = data_fixture.create_group()
@override_settings(DEBUG=True)
def test_delete_group(api_client, premium_data_fixture):
normal_user, normal_token = premium_data_fixture.create_user_and_token(
has_active_premium_license=True
)
staff_user, staff_token = premium_data_fixture.create_user_and_token(
is_staff=True, has_active_premium_license=True
)
group = premium_data_fixture.create_group()
url = reverse("api:premium:admin:groups:edit", kwargs={"group_id": 99999})
response = api_client.delete(url, HTTP_AUTHORIZATION=f"JWT {staff_token}")
@ -179,12 +204,22 @@ def test_delete_group(api_client, data_fixture):
assert response.status_code == 204
assert Group.objects.all().count() == 0
premium_data_fixture.remove_all_active_premium_licenses(staff_user)
group = premium_data_fixture.create_group()
url = reverse("api:premium:admin:groups:edit", kwargs={"group_id": group.id})
response = api_client.delete(url, HTTP_AUTHORIZATION=f"JWT {staff_token}")
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
assert response.json()["error"] == "ERROR_NO_ACTIVE_PREMIUM_LICENSE"
@pytest.mark.django_db
def test_cant_delete_template_group(api_client, data_fixture):
staff_user, staff_token = data_fixture.create_user_and_token(is_staff=True)
group = data_fixture.create_group()
data_fixture.create_template(group=group)
@override_settings(DEBUG=True)
def test_cant_delete_template_group(api_client, premium_data_fixture):
staff_user, staff_token = premium_data_fixture.create_user_and_token(
is_staff=True, has_active_premium_license=True
)
group = premium_data_fixture.create_group()
premium_data_fixture.create_template(group=group)
url = reverse("api:premium:admin:groups:edit", kwargs={"group_id": group.id})
response = api_client.delete(url, HTTP_AUTHORIZATION=f"JWT {staff_token}")

View file

@ -1,11 +1,14 @@
import json
import pytest
from django.shortcuts import reverse
from django.utils import timezone
from django.utils.datetime_safe import datetime
from django.test.utils import override_settings
from rest_framework.status import (
HTTP_200_OK,
HTTP_402_PAYMENT_REQUIRED,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED,
@ -29,9 +32,14 @@ invalid_passwords = [
@pytest.mark.django_db
def test_non_admin_cannot_see_admin_users_endpoint(api_client, data_fixture):
non_staff_user, token = data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1", is_staff=False
@override_settings(DEBUG=True)
def test_non_admin_cannot_see_admin_users_endpoint(api_client, premium_data_fixture):
non_staff_user, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=False,
has_active_premium_license=True,
)
response = api_client.get(
reverse("api:premium:admin:users:list"),
@ -42,22 +50,24 @@ def test_non_admin_cannot_see_admin_users_endpoint(api_client, data_fixture):
@pytest.mark.django_db
def test_admin_can_see_admin_users_endpoint(api_client, data_fixture):
staff_user, token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_admin_can_see_admin_users_endpoint(api_client, premium_data_fixture):
staff_user, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
has_active_premium_license=True,
)
group_user_is_admin_of = data_fixture.create_group()
data_fixture.create_user_group(
group_user_is_admin_of = premium_data_fixture.create_group()
premium_data_fixture.create_user_group(
group=group_user_is_admin_of,
user=staff_user,
permissions=GROUP_USER_PERMISSION_ADMIN,
)
group_user_is_not_admin_of = data_fixture.create_group()
data_fixture.create_user_group(
group_user_is_not_admin_of = premium_data_fixture.create_group()
premium_data_fixture.create_user_group(
group=group_user_is_not_admin_of,
user=staff_user,
permissions=GROUP_USER_PERMISSION_MEMBER,
@ -99,12 +109,35 @@ def test_admin_can_see_admin_users_endpoint(api_client, data_fixture):
@pytest.mark.django_db
def test_admin_with_invalid_token_cannot_see_admin_users(api_client, data_fixture):
data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_admin_list_users_without_premium_license(api_client, premium_data_fixture):
staff_user, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
)
response = api_client.get(
reverse("api:premium:admin:users:list"),
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
assert response.json()["error"] == "ERROR_NO_ACTIVE_PREMIUM_LICENSE"
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_with_invalid_token_cannot_see_admin_users(
api_client, premium_data_fixture
):
premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
response = api_client.get(
reverse("api:premium:admin:users:list"),
@ -116,14 +149,16 @@ def test_admin_with_invalid_token_cannot_see_admin_users(api_client, data_fixtur
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_accessing_invalid_user_admin_page_returns_error(
api_client, data_fixture
api_client, premium_data_fixture
):
_, token = data_fixture.create_user_and_token(
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:list")
response = api_client.get(
@ -136,14 +171,16 @@ def test_admin_accessing_invalid_user_admin_page_returns_error(
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_accessing_user_admin_with_invalid_page_size_returns_error(
api_client, data_fixture
api_client, premium_data_fixture
):
_, token = data_fixture.create_user_and_token(
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:list")
response = api_client.get(
@ -156,18 +193,21 @@ def test_admin_accessing_user_admin_with_invalid_page_size_returns_error(
@pytest.mark.django_db
def test_admin_can_search_users(api_client, data_fixture):
_, token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_admin_can_search_users(api_client, premium_data_fixture):
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
searched_for_user = data_fixture.create_user(
searched_for_user = premium_data_fixture.create_user(
email="specific_user@test.nl",
password="password",
first_name="Test1",
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:list")
response = api_client.get(
@ -196,18 +236,21 @@ def test_admin_can_search_users(api_client, data_fixture):
@pytest.mark.django_db
def test_admin_can_sort_users(api_client, data_fixture):
_, token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_admin_can_sort_users(api_client, premium_data_fixture):
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
searched_for_user = data_fixture.create_user(
searched_for_user = premium_data_fixture.create_user(
email="specific_user@test.nl",
password="password",
first_name="Test1",
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:list")
response = api_client.get(
@ -236,14 +279,16 @@ def test_admin_can_sort_users(api_client, data_fixture):
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_returns_error_response_if_invalid_sort_field_provided(
api_client, data_fixture
api_client, premium_data_fixture
):
_, token = data_fixture.create_user_and_token(
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:list")
response = api_client.get(
@ -256,14 +301,16 @@ def test_returns_error_response_if_invalid_sort_field_provided(
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_returns_error_response_if_sort_direction_not_provided(
api_client, data_fixture
api_client, premium_data_fixture
):
_, token = data_fixture.create_user_and_token(
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:list")
response = api_client.get(
@ -276,14 +323,16 @@ def test_returns_error_response_if_sort_direction_not_provided(
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_returns_error_response_if_invalid_sort_direction_provided(
api_client, data_fixture
api_client, premium_data_fixture
):
_, token = data_fixture.create_user_and_token(
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:list")
response = api_client.get(
@ -296,14 +345,16 @@ def test_returns_error_response_if_invalid_sort_direction_provided(
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_returns_error_response_if_invalid_sorts_mixed_with_valid_ones(
api_client, data_fixture
api_client, premium_data_fixture
):
_, token = data_fixture.create_user_and_token(
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:list")
response = api_client.get(
@ -316,12 +367,16 @@ def test_returns_error_response_if_invalid_sorts_mixed_with_valid_ones(
@pytest.mark.django_db
def test_returns_error_response_if_blank_sorts_provided(api_client, data_fixture):
_, token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_returns_error_response_if_blank_sorts_provided(
api_client, premium_data_fixture
):
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:list")
response = api_client.get(
@ -334,12 +389,14 @@ def test_returns_error_response_if_blank_sorts_provided(api_client, data_fixture
@pytest.mark.django_db
def test_returns_error_response_if_no_sorts_provided(api_client, data_fixture):
_, token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_returns_error_response_if_no_sorts_provided(api_client, premium_data_fixture):
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:list")
response = api_client.get(
@ -352,17 +409,20 @@ def test_returns_error_response_if_no_sorts_provided(api_client, data_fixture):
@pytest.mark.django_db
def test_non_admin_cannot_delete_user(api_client, data_fixture):
_, non_admin_token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_non_admin_cannot_delete_user(api_client, premium_data_fixture):
_, non_admin_token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=False,
has_active_premium_license=True,
)
user_to_delete = data_fixture.create_user(
user_to_delete = premium_data_fixture.create_user(
email="specific_user@test.nl",
password="password",
first_name="Test1",
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user_to_delete.id})
response = api_client.delete(
@ -374,19 +434,48 @@ def test_non_admin_cannot_delete_user(api_client, data_fixture):
@pytest.mark.django_db
def test_admin_can_delete_user(api_client, data_fixture):
_, token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_admin_cannot_delete_user_without_premium_license(
api_client, premium_data_fixture
):
_, admin_token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
)
user_to_delete = data_fixture.create_user(
user_to_delete = premium_data_fixture.create_user(
email="specific_user@test.nl",
password="password",
first_name="Test1",
)
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user_to_delete.id})
response = api_client.delete(
url,
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
assert response.json()["error"] == "ERROR_NO_ACTIVE_PREMIUM_LICENSE"
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_can_delete_user(api_client, premium_data_fixture):
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
user_to_delete = premium_data_fixture.create_user(
email="specific_user@test.nl",
password="password",
first_name="Test1",
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user_to_delete.id})
response = api_client.delete(
url,
format="json",
@ -404,12 +493,14 @@ def test_admin_can_delete_user(api_client, data_fixture):
@pytest.mark.django_db
def test_non_admin_cannot_patch_user(api_client, data_fixture):
non_admin_user, non_admin_user_token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_non_admin_cannot_patch_user(api_client, premium_data_fixture):
non_admin_user, non_admin_user_token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=False,
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": non_admin_user.id})
response = api_client.patch(
@ -425,13 +516,40 @@ def test_non_admin_cannot_patch_user(api_client, data_fixture):
@pytest.mark.django_db
def test_admin_can_patch_user(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_non_admin_cannot_patch_user_without_premium_license(
api_client, premium_data_fixture
):
non_admin_user, non_admin_user_token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
)
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": non_admin_user.id})
response = api_client.patch(
url,
{"username": "some_other_email@test.nl"},
format="json",
HTTP_AUTHORIZATION=f"JWT {non_admin_user_token}",
)
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
assert response.json()["error"] == "ERROR_NO_ACTIVE_PREMIUM_LICENSE"
non_admin_user.refresh_from_db()
assert non_admin_user.email == "test@test.nl"
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_can_patch_user(api_client, premium_data_fixture):
user, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user.id})
old_password = user.password
@ -457,13 +575,17 @@ def test_admin_can_patch_user(api_client, data_fixture):
@pytest.mark.django_db
def test_admin_can_patch_user_without_providing_password(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_admin_can_patch_user_without_providing_password(
api_client, premium_data_fixture
):
user, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user.id})
old_password = user.password
@ -489,17 +611,21 @@ def test_admin_can_patch_user_without_providing_password(api_client, data_fixtur
@pytest.mark.django_db
@override_settings(DEBUG=True)
@pytest.mark.parametrize("invalid_password", invalid_passwords)
def test_invalid_password_returns_400(api_client, data_fixture, invalid_password):
user, token = data_fixture.create_user_and_token(
def test_invalid_password_returns_400(
api_client, premium_data_fixture, invalid_password
):
user, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
has_active_premium_license=True,
)
user_to_edit = data_fixture.create_user(
user_to_edit = premium_data_fixture.create_user(
email="second@test.nl",
password="password",
first_name="Test1",
@ -534,13 +660,17 @@ def test_invalid_password_returns_400(api_client, data_fixture, invalid_password
@pytest.mark.django_db
def test_error_returned_when_invalid_field_supplied_to_edit(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_error_returned_when_invalid_field_supplied_to_edit(
api_client, premium_data_fixture
):
user, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user.id})
@ -558,13 +688,17 @@ def test_error_returned_when_invalid_field_supplied_to_edit(api_client, data_fix
@pytest.mark.django_db
def test_error_returned_when_updating_user_with_invalid_email(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
@override_settings(DEBUG=True)
def test_error_returned_when_updating_user_with_invalid_email(
api_client, premium_data_fixture
):
user, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user.id})
@ -582,15 +716,17 @@ def test_error_returned_when_updating_user_with_invalid_email(api_client, data_f
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_error_returned_when_valid_and_invalid_fields_supplied_to_edit(
api_client, data_fixture
api_client, premium_data_fixture
):
user, token = data_fixture.create_user_and_token(
user, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
has_active_premium_license=True,
)
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user.id})
@ -608,19 +744,21 @@ def test_error_returned_when_valid_and_invalid_fields_supplied_to_edit(
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_getting_view_users_only_runs_two_queries_instead_of_n(
data_fixture, django_assert_num_queries, api_client
premium_data_fixture, django_assert_num_queries, api_client
):
_, token = data_fixture.create_user_and_token(
_, token = premium_data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
has_active_premium_license=True,
)
fixed_num_of_queries_unrelated_to_number_of_rows = 5
fixed_num_of_queries_unrelated_to_number_of_rows = 6
for i in range(10):
data_fixture.create_user_group()
premium_data_fixture.create_user_group()
with django_assert_num_queries(fixed_num_of_queries_unrelated_to_number_of_rows):
response = api_client.get(
@ -633,7 +771,7 @@ def test_admin_getting_view_users_only_runs_two_queries_instead_of_n(
# Make even more to ensure that more rows don't result in more queries.
for i in range(10):
data_fixture.create_user_group()
premium_data_fixture.create_user_group()
with django_assert_num_queries(fixed_num_of_queries_unrelated_to_number_of_rows):
response = api_client.get(

View file

@ -1,33 +1,46 @@
from unittest.mock import patch
import pytest
from django.core.files.storage import FileSystemStorage
from django.urls import reverse
from django.utils.dateparse import parse_datetime
from django.utils.timezone import utc, make_aware
from django.test.utils import override_settings
from freezegun import freeze_time
from rest_framework.status import HTTP_402_PAYMENT_REQUIRED
from rest_framework.fields import DateTimeField
from baserow.contrib.database.rows.handler import RowHandler
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_exporting_json_writes_file_to_storage(
data_fixture, api_client, tmpdir, settings, django_capture_on_commit_callbacks
premium_data_fixture,
api_client,
tmpdir,
settings,
django_capture_on_commit_callbacks,
):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(table=table, name="text_field", order=0)
option_field = data_fixture.create_single_select_field(
user, token = premium_data_fixture.create_user_and_token(
has_active_premium_license=True
)
table = premium_data_fixture.create_database_table(user=user)
text_field = premium_data_fixture.create_text_field(
table=table, name="text_field", order=0
)
option_field = premium_data_fixture.create_single_select_field(
table=table, name="option_field", order=1
)
option_a = data_fixture.create_select_option(
option_a = premium_data_fixture.create_select_option(
field=option_field, value="A", color="blue"
)
option_b = data_fixture.create_select_option(
option_b = premium_data_fixture.create_select_option(
field=option_field, value="B", color="red"
)
date_field = data_fixture.create_date_field(
date_field = premium_data_fixture.create_date_field(
table=table,
date_include_time=True,
date_format="US",
@ -35,11 +48,11 @@ def test_exporting_json_writes_file_to_storage(
order=2,
)
grid_view = data_fixture.create_grid_view(table=table)
data_fixture.create_view_filter(
grid_view = premium_data_fixture.create_grid_view(table=table)
premium_data_fixture.create_view_filter(
view=grid_view, field=text_field, type="contains", value="test"
)
data_fixture.create_view_sort(view=grid_view, field=text_field, order="ASC")
premium_data_fixture.create_view_sort(view=grid_view, field=text_field, order="ASC")
row_handler = RowHandler()
row_handler.create_row(
@ -135,24 +148,50 @@ def test_exporting_json_writes_file_to_storage(
real = written_file.read()
assert real == expected
premium_data_fixture.remove_all_active_premium_licenses(user)
response = api_client.post(
reverse(
"api:database:export:export_table",
kwargs={"table_id": table.id},
),
data={
"view_id": grid_view.id,
"exporter_type": "json",
"json_charset": "utf-8",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
assert response.json()["error"] == "ERROR_NO_ACTIVE_PREMIUM_LICENSE"
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_exporting_xml_writes_file_to_storage(
data_fixture, api_client, tmpdir, settings, django_capture_on_commit_callbacks
premium_data_fixture,
api_client,
tmpdir,
settings,
django_capture_on_commit_callbacks,
):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(table=table, name="text_field", order=0)
option_field = data_fixture.create_single_select_field(
user, token = premium_data_fixture.create_user_and_token(
has_active_premium_license=True
)
table = premium_data_fixture.create_database_table(user=user)
text_field = premium_data_fixture.create_text_field(
table=table, name="text_field", order=0
)
option_field = premium_data_fixture.create_single_select_field(
table=table, name="option_field", order=1
)
option_a = data_fixture.create_select_option(
option_a = premium_data_fixture.create_select_option(
field=option_field, value="A", color="blue"
)
option_b = data_fixture.create_select_option(
option_b = premium_data_fixture.create_select_option(
field=option_field, value="B", color="red"
)
date_field = data_fixture.create_date_field(
date_field = premium_data_fixture.create_date_field(
table=table,
date_include_time=True,
date_format="US",
@ -160,11 +199,11 @@ def test_exporting_xml_writes_file_to_storage(
order=2,
)
grid_view = data_fixture.create_grid_view(table=table)
data_fixture.create_view_filter(
grid_view = premium_data_fixture.create_grid_view(table=table)
premium_data_fixture.create_view_filter(
view=grid_view, field=text_field, type="contains", value="test"
)
data_fixture.create_view_sort(view=grid_view, field=text_field, order="ASC")
premium_data_fixture.create_view_sort(view=grid_view, field=text_field, order="ASC")
row_handler = RowHandler()
row_handler.create_row(
@ -263,6 +302,23 @@ def test_exporting_xml_writes_file_to_storage(
expected
)
premium_data_fixture.remove_all_active_premium_licenses(user)
response = api_client.post(
reverse(
"api:database:export:export_table",
kwargs={"table_id": table.id},
),
data={
"view_id": grid_view.id,
"exporter_type": "xml",
"xml_charset": "utf-8",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
assert response.json()["error"] == "ERROR_NO_ACTIVE_PREMIUM_LICENSE"
def strip_indents_and_newlines(xml):
return "".join([line.strip() for line in xml.split("\n")])

View file

@ -0,0 +1,775 @@
import pytest
import responses
from freezegun import freeze_time
from django.shortcuts import reverse
from django.test.utils import override_settings
from django.utils.timezone import make_aware, datetime, utc
from rest_framework.status import (
HTTP_200_OK,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_403_FORBIDDEN,
HTTP_404_NOT_FOUND,
)
from baserow_premium.license.models import License, LicenseUser
VALID_ONE_SEAT_LICENSE = (
# id: "1", instance_id: "1"
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogIjEiLCAidmFsaWRfZnJvbSI6ICIyMDIxLTA4LTI5VDE5OjUyOjU3"
b"Ljg0MjY5NiIsICJ2YWxpZF90aHJvdWdoIjogIjIwMjEtMDktMjlUMTk6NTI6NTcuODQyNjk2IiwgInBy"
b"b2R1Y3RfY29kZSI6ICJwcmVtaXVtIiwgInNlYXRzIjogMSwgImlzc3VlZF9vbiI6ICIyMDIxLTA4LTI5"
b"VDE5OjUyOjU3Ljg0MjY5NiIsICJpc3N1ZWRfdG9fZW1haWwiOiAiYnJhbUBiYXNlcm93LmlvIiwgImlz"
b"c3VlZF90b19uYW1lIjogIkJyYW0iLCAiaW5zdGFuY2VfaWQiOiAiMSJ9.e33Z4CxLSmD-R55Es24P3mR"
b"8Oqn3LpaXvgYLzF63oFHat3paon7IBjBmOX3eyd8KjirVf3empJds4uUw2Nn2m7TVvRAtJ8XzNl-8ytf"
b"2RLtmjMx1Xkgp5VZ8S7UqJ_cKLyl76eVRtGEA1DH2HdPKu1vBPJ4bzDfnhDPYl4k5z9XSSgqAbQ9WO0U"
b"5kiI3BYjVRZSKnZMeguAGZ47ezDj_WArGcHAB8Pa2v3HFp5Y34DMJ8r3_hD5hxCKgoNx4AHx1Q-hRDqp"
b"Aroj-4jl7KWvlP-OJNc1BgH2wnhFmeKHotv-Iumi83JQohyceUbG6j8rDDQvJfcn0W2_ebmUH3TKr-w="
b"="
)
VALID_TWO_SEAT_LICENSE = (
# id: "2", instance_id: "1"
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogIjIiLCAidmFsaWRfZnJvbSI6ICIyMDIxLTA4LTI5VDE5OjUzOjM3"
b"LjA5MjMwMyIsICJ2YWxpZF90aHJvdWdoIjogIjIwMjEtMDktMjlUMTk6NTM6MzcuMDkyMzAzIiwgInBy"
b"b2R1Y3RfY29kZSI6ICJwcmVtaXVtIiwgInNlYXRzIjogMiwgImlzc3VlZF9vbiI6ICIyMDIxLTA4LTI5"
b"VDE5OjUzOjM3LjA5MjMwMyIsICJpc3N1ZWRfdG9fZW1haWwiOiAiYnJhbUBiYXNlcm93LmlvIiwgImlz"
b"c3VlZF90b19uYW1lIjogIkJyYW0iLCAiaW5zdGFuY2VfaWQiOiAiMSJ9.d41tB1kx69gw-9xDrRI0kER"
b"KDUtR-P6yRM3ufKZ_XRDewVCBAniCLe9-ce7TKabnMedE2cqHjYVLlI66Dfa5oH8fGswnyC16c9ZHlOU"
b"jQ5CpHTorZm6eyCXaP6MDdhstCNKdDrZns3qvVMAqDpmxS8wmiG9Y6gZjvBGXZWeoCraF1SVcUnFBBlf"
b"UemfGSQUwPitVlxJ6GWN-hzi7b1GZqWJKDb2YYJ0T30VMJeNO7oi6YHMUOH33041FU79DSET2A2NNEFu"
b"e-jnCcw5NFpH-zGzBDv1wpR3DFmJa78KwGbj0Kdzim85AUzi1xGRlIyxxTdTkVy2B-08lPaoG8Q62bw="
b"="
)
VALID_INSTANCE_TWO_LICENSE = (
# id: "2", instance_id: "2"
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogIjEiLCAidmFsaWRfZnJvbSI6ICIyMDIxLTA4LTI5VDE5OjUyOjU3"
b"Ljg0MjY5NiIsICJ2YWxpZF90aHJvdWdoIjogIjIwMjEtMDktMjlUMTk6NTI6NTcuODQyNjk2IiwgInBy"
b"b2R1Y3RfY29kZSI6ICJwcmVtaXVtIiwgInNlYXRzIjogMSwgImlzc3VlZF9vbiI6ICIyMDIxLTA4LTI5"
b"VDE5OjUyOjU3Ljg0MjY5NiIsICJpc3N1ZWRfdG9fZW1haWwiOiAiYnJhbUBiYXNlcm93LmlvIiwgImlz"
b"c3VlZF90b19uYW1lIjogIkJyYW0iLCAiaW5zdGFuY2VfaWQiOiAiMiJ9.i3Og4ZJwz__TxWyFc2B6lDi"
b"ZBAIOVTZv_jXVzQQqcjG-flPAicqXFECl7MbbexVmtsMES-U7VPebOh0t4oPoDXL1LiftfjmT63wO4An"
b"A3FMS0Ip0GIx2upkQC-MlU1kSR9Tltrr1qySuQvXORDRUaSxaRQQacwZTOIviVdcxG9vesjkFwn6LMYp"
b"-GhmCJXB0YfMgsvPm6kj6qTWPh3ed8aLNFnekUhB-dUwA4tqPicCQHRQCRZqzo9vx-hKdeHCGZMg0htG"
b"EB4cAeV4I29JXPC83qtwt6DSCPxudlJsli3tYsLMcxAHysVN3H_FAY8qg54MP33OKvZuwww5uFDITMQ="
b"="
)
INVALID_SIGNATURE_LICENSE = (
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogMSwgInZhbGlkX2Zyb20iOiAiMjAyMS0wOC0yOVQxOTo1NDoxMi4w"
b"NjY4NDYiLCAidmFsaWRfdGhyb3VnaCI6ICIyMDIxLTA5LTI5VDE5OjU0OjEyLjA2Njg0NiIsICJwcm9k"
b"dWN0X2NvZGUiOiAicHJlbWl1bSIsICJzZWF0cyI6IDEsICJ0b19lbWFpbCI6ICJicmFtQGJhc2Vyb3cu"
b"aW8iLCAidG9fbmFtZSI6ICJCcmFtIn0=.hYaWGO0M6s1pA9bhcBlk1fE1QMrhlDGNiBIBG_2O2AMGFPj"
b"gnsHdwfUIe_eo6dyAyvsToxBrpxr6N1vRqPdA61cKjTlhUdFqvj7NTeydS4Z9TlfP-vFslQk9CO_ok7Z"
b"ws8AHTQ2pKfsdzqcWNZnWKZeQGEtO73MIoFJbHr07mtWA1ZZgJNBTBpp-7BNtvj2bQyUeXyRKD5LVj8G"
b"ESDcapZCNt5ufesbYvpfs1c6p6UP4z3gszOYrzMApMqWHty7j10SDjcLIEsUTd02r_Pbip-KxmGfecXg"
b"B0HF7HJZwkY9ZdlZ7ODGtV0e455dQwh5sSHa3RRd71AXVou-cuOS87g=="
)
INVALID_VERSION_LICENSE = (
b"eyJ2ZXJzaW9uIjogOTk5OX0=.rzAyL6qBkz_Eb3GYaSOXy9CJ2HJg4uAxtrbinh4aDYy7Eq4e4RpfaPm"
b"4dZLocIRxSmx_wUYSI0CMqmkwABHgzxRVmzVAmXf5MxX7vAGjjEnQX_dQOl8kY15gXhEQZv5pjSPVcZW"
b"CLll95OFoBUJhtOQqNC6JLA1LZdiSPG6zFhvi5V27sRGBz3E8jhFLWY-Y2WIq5_9q2d_hVFM0KHwRcxb"
b"CVof8RBUq1DgMcDKEGE7WRHYDVP1QugBjf4GZlvIE4ZVr3tKr0aKPX8nuNVhbQeudCW8tnturmxevpRN"
b"vLS5ETSQzJoP46cGuw0HUV20P4SnvQP_NRd5zifgllJqsUw=="
)
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_list_licenses(api_client, data_fixture, django_assert_num_queries):
user_1 = data_fixture.create_user()
user_2 = data_fixture.create_user()
user_3 = data_fixture.create_user()
license_1 = License.objects.create(license=VALID_ONE_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license_1, user=user_1)
license_2 = License.objects.create(
license=VALID_TWO_SEAT_LICENSE.decode(),
last_check=make_aware(datetime(2021, 8, 29, 19, 52, 57, 842696), utc),
)
LicenseUser.objects.create(license=license_2, user=user_2)
LicenseUser.objects.create(license=license_2, user=user_3)
with freeze_time("2021-09-01 00:00"):
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False,
)
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
)
response = api_client.get(
reverse("api:premium:license:list"),
format="json",
HTTP_AUTHORIZATION=f"JWT {normal_token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
# We expect one query for the user check and one for the fetching the
# licenses, including the count of seats that are taken.
with django_assert_num_queries(2):
response = api_client.get(
reverse("api:premium:license:list"),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert len(response_json) == 2
assert response_json[0]["license_id"] == "1"
assert response_json[0]["is_active"] is True
assert response_json[0]["last_check"] is None
assert response_json[0]["valid_from"] == "2021-08-29T19:52:57.842696Z"
assert response_json[0]["valid_through"] == "2021-09-29T19:52:57.842696Z"
assert response_json[0]["seats_taken"] == 1
assert response_json[0]["seats"] == 1
assert response_json[0]["product_code"] == "premium"
assert response_json[0]["issued_on"] == "2021-08-29T19:52:57.842696Z"
assert response_json[0]["issued_to_email"] == "bram@baserow.io"
assert response_json[0]["issued_to_name"] == "Bram"
assert response_json[1]["license_id"] == "2"
assert response_json[1]["is_active"] is True
assert response_json[1]["last_check"] == "2021-08-29T19:52:57.842696Z"
assert response_json[1]["valid_from"] == "2021-08-29T19:53:37.092303Z"
assert response_json[1]["valid_through"] == "2021-09-29T19:53:37.092303Z"
assert response_json[1]["seats_taken"] == 2
assert response_json[1]["seats"] == 2
assert response_json[1]["issued_on"] == "2021-08-29T19:53:37.092303Z"
assert response_json[1]["issued_to_email"] == "bram@baserow.io"
assert response_json[1]["issued_to_name"] == "Bram"
assert response_json[1]["product_code"] == "premium"
with freeze_time("2021-09-29 19:53"):
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
)
response = api_client.get(
reverse("api:premium:license:list"),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert len(response_json) == 2
assert response_json[0]["license_id"] == "2"
assert response_json[0]["is_active"] is True
assert response_json[0]["last_check"] == "2021-08-29T19:52:57.842696Z"
assert response_json[0]["valid_from"] == "2021-08-29T19:53:37.092303Z"
assert response_json[0]["valid_through"] == "2021-09-29T19:53:37.092303Z"
assert response_json[0]["seats_taken"] == 2
assert response_json[0]["seats"] == 2
assert response_json[0]["product_code"] == "premium"
assert response_json[0]["issued_on"] == "2021-08-29T19:53:37.092303Z"
assert response_json[0]["issued_to_email"] == "bram@baserow.io"
assert response_json[0]["issued_to_name"] == "Bram"
assert response_json[1]["license_id"] == "1"
assert response_json[1]["is_active"] is False
assert response_json[1]["last_check"] is None
assert response_json[1]["valid_from"] == "2021-08-29T19:52:57.842696Z"
assert response_json[1]["valid_through"] == "2021-09-29T19:52:57.842696Z"
assert response_json[1]["seats_taken"] == 1
assert response_json[1]["seats"] == 1
assert response_json[1]["product_code"] == "premium"
assert response_json[1]["issued_on"] == "2021-08-29T19:52:57.842696Z"
assert response_json[1]["issued_to_email"] == "bram@baserow.io"
assert response_json[1]["issued_to_name"] == "Bram"
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_admin_register_license(api_client, data_fixture):
data_fixture.update_settings(instance_id="1")
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False,
)
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
)
response = api_client.post(
reverse("api:premium:license:list"),
{"license": VALID_ONE_SEAT_LICENSE.decode()},
format="json",
HTTP_AUTHORIZATION=f"JWT {normal_token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
response = api_client.post(
reverse("api:premium:license:list"),
{"license": INVALID_SIGNATURE_LICENSE.decode()},
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_INVALID_PREMIUM_LICENSE"
with freeze_time("2021-10-01 00:00"):
admin_token = data_fixture.generate_token(admin_user)
response = api_client.post(
reverse("api:premium:license:list"),
{"license": VALID_ONE_SEAT_LICENSE.decode()},
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_HAS_EXPIRED"
with freeze_time("2021-09-01 00:00"):
admin_token = data_fixture.generate_token(admin_user)
response = api_client.post(
reverse("api:premium:license:list"),
{"license": INVALID_VERSION_LICENSE.decode()},
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_UNSUPPORTED_PREMIUM_LICENSE"
response = api_client.post(
reverse("api:premium:license:list"),
{"license": VALID_INSTANCE_TWO_LICENSE.decode()},
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_INSTANCE_ID_MISMATCH"
response = api_client.post(
reverse("api:premium:license:list"),
{"license": VALID_TWO_SEAT_LICENSE.decode()},
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
licenses = License.objects.all()
assert len(licenses) == 1
response_json = response.json()
assert response_json["id"] == licenses[0].id
assert response_json["license_id"] == "2"
assert response_json["is_active"] is True
assert response_json["last_check"] is None
assert response_json["valid_from"] == "2021-08-29T19:53:37.092303Z"
assert response_json["valid_through"] == "2021-09-29T19:53:37.092303Z"
assert response_json["seats_taken"] == 0
assert response_json["seats"] == 2
assert response_json["product_code"] == "premium"
assert response_json["issued_on"] == "2021-08-29T19:53:37.092303Z"
assert response_json["issued_to_email"] == "bram@baserow.io"
assert response_json["issued_to_name"] == "Bram"
response = api_client.post(
reverse("api:premium:license:list"),
{"license": VALID_TWO_SEAT_LICENSE.decode()},
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_ALREADY_EXISTS"
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_get_license(api_client, data_fixture, django_assert_num_queries):
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False,
)
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
)
license = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license, user=normal_user)
LicenseUser.objects.create(license=license, user=admin_user)
response = api_client.get(
reverse("api:premium:license:item", kwargs={"id": license.id}),
format="json",
HTTP_AUTHORIZATION=f"JWT {normal_token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
response = api_client.get(
reverse("api:premium:license:item", kwargs={"id": 0}),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"
with freeze_time("2021-09-01 00:00"):
with django_assert_num_queries(4):
response = api_client.get(
reverse("api:premium:license:item", kwargs={"id": license.id}),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["id"] == license.id
assert response_json["license_id"] == "2"
assert response_json["is_active"] is True
assert response_json["last_check"] is None
assert response_json["valid_from"] == "2021-08-29T19:53:37.092303Z"
assert response_json["valid_through"] == "2021-09-29T19:53:37.092303Z"
assert response_json["seats_taken"] == 2
assert response_json["seats"] == 2
assert response_json["product_code"] == "premium"
assert response_json["issued_on"] == "2021-08-29T19:53:37.092303Z"
assert response_json["issued_to_email"] == "bram@baserow.io"
assert response_json["issued_to_name"] == "Bram"
assert len(response_json["users"]) == 2
assert response_json["users"][0]["id"] == normal_user.id
assert response_json["users"][0]["email"] == normal_user.email
assert response_json["users"][0]["first_name"] == normal_user.first_name
assert response_json["users"][1]["id"] == admin_user.id
assert response_json["users"][1]["email"] == admin_user.email
assert response_json["users"][1]["first_name"] == admin_user.first_name
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_delete_license(api_client, data_fixture):
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False,
)
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
)
license = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
response = api_client.delete(
reverse("api:premium:license:item", kwargs={"id": license.id}),
format="json",
HTTP_AUTHORIZATION=f"JWT {normal_token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
response = api_client.delete(
reverse("api:premium:license:item", kwargs={"id": 0}),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"
response = api_client.delete(
reverse("api:premium:license:item", kwargs={"id": license.id}),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_204_NO_CONTENT
assert License.objects.all().count() == 0
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_add_user_to_license(api_client, data_fixture):
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False,
)
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
)
license = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
response = api_client.post(
reverse(
"api:premium:license:user",
kwargs={"id": license.id, "user_id": normal_user.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {normal_token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
response = api_client.post(
reverse(
"api:premium:license:user",
kwargs={"id": license.id, "user_id": 0},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_USER_NOT_FOUND"
response = api_client.post(
reverse(
"api:premium:license:user",
kwargs={"id": 0, "user_id": normal_user.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"
response = api_client.post(
reverse(
"api:premium:license:user",
kwargs={"id": license.id, "user_id": normal_user.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["id"] == normal_user.id
assert response_json["first_name"] == normal_user.first_name
assert response_json["email"] == normal_user.email
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_delete_user_from_license(api_client, data_fixture):
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False,
)
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
)
license = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license, user=normal_user)
response = api_client.delete(
reverse(
"api:premium:license:user",
kwargs={"id": license.id, "user_id": normal_user.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {normal_token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
response = api_client.delete(
reverse(
"api:premium:license:user",
kwargs={"id": license.id, "user_id": 0},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_USER_NOT_FOUND"
response = api_client.delete(
reverse(
"api:premium:license:user",
kwargs={"id": 0, "user_id": normal_user.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"
response = api_client.delete(
reverse(
"api:premium:license:user",
kwargs={"id": license.id, "user_id": normal_user.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_204_NO_CONTENT
assert LicenseUser.objects.all().count() == 0
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_fill_users_in_license(api_client, data_fixture):
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False,
)
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
)
license = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
response = api_client.post(
reverse(
"api:premium:license:fill_seats",
kwargs={"id": license.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {normal_token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
response = api_client.post(
reverse(
"api:premium:license:fill_seats",
kwargs={"id": 0},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"
response = api_client.post(
reverse(
"api:premium:license:fill_seats",
kwargs={"id": license.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
print(response_json)
assert len(response_json) == 2
assert response_json[0]["id"] == normal_user.id
assert response_json[0]["email"] == normal_user.email
assert response_json[0]["first_name"] == normal_user.first_name
assert response_json[1]["id"] == admin_user.id
assert response_json[1]["email"] == admin_user.email
assert response_json[1]["first_name"] == admin_user.first_name
assert LicenseUser.objects.all().count() == 2
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_remove_all_users(api_client, data_fixture):
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False,
)
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
)
license = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license, user=normal_user)
LicenseUser.objects.create(license=license, user=admin_user)
response = api_client.post(
reverse(
"api:premium:license:remove_all_users",
kwargs={"id": license.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {normal_token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
response = api_client.post(
reverse(
"api:premium:license:remove_all_users",
kwargs={"id": 0},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"
response = api_client.post(
reverse(
"api:premium:license:remove_all_users",
kwargs={"id": license.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_204_NO_CONTENT
assert LicenseUser.objects.all().count() == 0
@pytest.mark.django_db
def test_admin_license_user_lookup(api_client, data_fixture):
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False, first_name="Test", email="email@localhost"
)
normal_user_2 = data_fixture.create_user(first_name="Foo", email="tmp@localhost")
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True, first_name="Admin test", email="admin@localhost"
)
license = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license, user=admin_user)
license_2 = License.objects.create(license=VALID_ONE_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license_2, user=normal_user)
response = api_client.get(
reverse(
"api:premium:license:lookup_users",
kwargs={"id": license.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {normal_token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
response = api_client.get(
reverse(
"api:premium:license:lookup_users",
kwargs={"id": 0},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"
response = api_client.get(
reverse(
"api:premium:license:lookup_users",
kwargs={"id": license.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["count"] == 2
assert len(response_json["results"]) == 2
assert response_json["results"][0]["id"] == normal_user_2.id
assert response_json["results"][0]["value"] == "Foo (tmp@localhost)"
url = reverse(
"api:premium:license:lookup_users",
kwargs={"id": license.id},
)
response = api_client.get(
f"{url}?page=2",
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_INVALID_PAGE"
url = reverse(
"api:premium:license:lookup_users",
kwargs={"id": license.id},
)
response = api_client.get(
f"{url}?page=2&size=1",
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["count"] == 2
assert len(response_json["results"]) == 1
assert response_json["results"][0]["id"] == normal_user.id
assert response_json["results"][0]["value"] == "Test (email@localhost)"
url = reverse(
"api:premium:license:lookup_users",
kwargs={"id": license.id},
)
response = api_client.get(
f"{url}?search=test",
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["count"] == 1
assert len(response_json["results"]) == 1
assert response_json["results"][0]["id"] == normal_user.id
assert response_json["results"][0]["value"] == "Test (email@localhost)"
url = reverse(
"api:premium:license:lookup_users",
kwargs={"id": license.id},
)
response = api_client.get(
f"{url}?search=email",
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["count"] == 1
assert len(response_json["results"]) == 1
assert response_json["results"][0]["id"] == normal_user.id
assert response_json["results"][0]["value"] == "Test (email@localhost)"
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_admin_check_license(api_client, data_fixture):
normal_user, normal_token = data_fixture.create_user_and_token(
is_staff=False,
)
admin_user, admin_token = data_fixture.create_user_and_token(
is_staff=True,
)
license_1 = License.objects.create(license=VALID_ONE_SEAT_LICENSE.decode())
license_2 = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
response = api_client.get(
reverse(
"api:premium:license:check",
kwargs={"id": license_1.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {normal_token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
response = api_client.get(
reverse(
"api:premium:license:check",
kwargs={"id": 0},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_PREMIUM_LICENSE_DOES_NOT_EXIST"
with freeze_time("2021-07-01 12:00"):
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
json={
VALID_ONE_SEAT_LICENSE.decode(): {
"type": "invalid",
"detail": "",
},
VALID_TWO_SEAT_LICENSE.decode(): {
"type": "ok",
"detail": "",
},
},
status=200,
)
response = api_client.get(
reverse(
"api:premium:license:check",
kwargs={"id": license_1.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_204_NO_CONTENT
response = api_client.get(
reverse(
"api:premium:license:check",
kwargs={"id": license_2.id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {admin_token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["id"] == license_2.id
assert response_json["license_id"] == "2"
assert len(response_json["users"]) == 0

View file

@ -1,8 +1,14 @@
import pytest
from django.conf import settings
from django.test.utils import override_settings
from freezegun import freeze_time
from rest_framework.reverse import reverse
from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND
from rest_framework.status import (
HTTP_200_OK,
HTTP_402_PAYMENT_REQUIRED,
HTTP_400_BAD_REQUEST,
HTTP_404_NOT_FOUND,
)
from baserow.core.models import TrashEntry
from baserow.core.trash.handler import TrashHandler
@ -10,9 +16,12 @@ from baserow_premium.row_comments.models import RowComment
@pytest.mark.django_db
def test_row_comments_api_view(data_fixture, api_client):
user, token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
@override_settings(DEBUG=True)
def test_row_comments_api_view(premium_data_fixture, api_client):
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row", "second_row"], user=user
)
@ -73,8 +82,35 @@ def test_row_comments_api_view(data_fixture, api_client):
@pytest.mark.django_db
def test_row_comments_cant_view_comments_for_invalid_table(data_fixture, api_client):
user, token = data_fixture.create_user_and_token(first_name="Test User")
@override_settings(DEBUG=True)
def test_row_comments_api_view_without_premium_license(
premium_data_fixture, api_client
):
user, token = premium_data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row", "second_row"], user=user
)
response = api_client.get(
reverse(
"api:premium:row_comments:item",
kwargs={"table_id": table.id, "row_id": rows[0].id},
),
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
assert response.json()["error"] == "ERROR_NO_ACTIVE_PREMIUM_LICENSE"
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_row_comments_cant_view_comments_for_invalid_table(
premium_data_fixture, api_client
):
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
response = api_client.get(
reverse(
@ -89,11 +125,14 @@ def test_row_comments_cant_view_comments_for_invalid_table(data_fixture, api_cli
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_row_comments_cant_view_comments_for_invalid_row_in_table(
data_fixture, api_client
premium_data_fixture, api_client
):
user, token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)
@ -110,12 +149,17 @@ def test_row_comments_cant_view_comments_for_invalid_row_in_table(
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_row_comments_users_cant_view_comments_for_table_they_are_not_in_group_for(
data_fixture, api_client
premium_data_fixture, api_client
):
user, token = data_fixture.create_user_and_token(first_name="Test User")
other_user, other_token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
other_user, other_token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)
@ -142,8 +186,13 @@ def test_row_comments_users_cant_view_comments_for_table_they_are_not_in_group_f
@pytest.mark.django_db
def test_row_comments_cant_create_comments_in_invalid_table(data_fixture, api_client):
user, token = data_fixture.create_user_and_token(first_name="Test User")
@override_settings(DEBUG=True)
def test_row_comments_cant_create_comments_in_invalid_table(
premium_data_fixture, api_client
):
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
response = api_client.post(
reverse(
@ -159,11 +208,14 @@ def test_row_comments_cant_create_comments_in_invalid_table(data_fixture, api_cl
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_row_comments_cant_create_comments_in_invalid_row_in_table(
data_fixture, api_client
premium_data_fixture, api_client
):
user, token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)
@ -181,12 +233,17 @@ def test_row_comments_cant_create_comments_in_invalid_row_in_table(
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_row_comments_users_cant_create_comments_for_table_they_are_not_in_group_for(
data_fixture, api_client
premium_data_fixture, api_client
):
user, token = data_fixture.create_user_and_token(first_name="Test User")
other_user, other_token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
other_user, other_token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)
@ -215,9 +272,12 @@ def test_row_comments_users_cant_create_comments_for_table_they_are_not_in_group
@pytest.mark.django_db
def test_cant_make_a_blank_row_comment(data_fixture, api_client):
user, token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
@override_settings(DEBUG=True)
def test_cant_make_a_blank_row_comment(premium_data_fixture, api_client):
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)
@ -238,9 +298,14 @@ def test_cant_make_a_blank_row_comment(data_fixture, api_client):
@pytest.mark.django_db
def test_cant_make_a_row_comment_greater_than_max_settings(data_fixture, api_client):
user, token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
@override_settings(DEBUG=True)
def test_cant_make_a_row_comment_greater_than_max_settings(
premium_data_fixture, api_client
):
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)
response = api_client.post(
@ -277,9 +342,12 @@ def test_cant_make_a_row_comment_greater_than_max_settings(data_fixture, api_cli
@pytest.mark.django_db
def test_cant_make_a_null_row_comment(data_fixture, api_client):
user, token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
@override_settings(DEBUG=True)
def test_cant_make_a_null_row_comment(premium_data_fixture, api_client):
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)
@ -300,9 +368,33 @@ def test_cant_make_a_null_row_comment(data_fixture, api_client):
@pytest.mark.django_db
def test_trashing_the_row_returns_404_for_comments(data_fixture, api_client):
user, token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
@override_settings(DEBUG=True)
def test_cant_make_a_row_without_premium_license(premium_data_fixture, api_client):
user, token = premium_data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)
response = api_client.post(
reverse(
"api:premium:row_comments:item",
kwargs={"table_id": table.id, "row_id": rows[0].id},
),
{"comment": "test"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
assert response.json()["error"] == "ERROR_NO_ACTIVE_PREMIUM_LICENSE"
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_trashing_the_row_returns_404_for_comments(premium_data_fixture, api_client):
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)
@ -367,11 +459,14 @@ def test_trashing_the_row_returns_404_for_comments(data_fixture, api_client):
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_perm_deleting_a_trashed_row_with_comments_cleans_up_the_rows(
data_fixture, api_client
premium_data_fixture, api_client
):
user, token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row", "second row"], user=user
)
@ -424,14 +519,17 @@ def test_perm_deleting_a_trashed_row_with_comments_cleans_up_the_rows(
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_perm_deleting_a_trashed_table_with_comments_cleans_up_the_rows(
data_fixture, api_client
premium_data_fixture, api_client
):
user, token = data_fixture.create_user_and_token(first_name="Test User")
table, fields, rows = data_fixture.build_table(
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row", "second row"], user=user
)
other_table, other_fields, other_rows = data_fixture.build_table(
other_table, other_fields, other_rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row", "second row"], user=user
)
@ -483,17 +581,22 @@ def test_perm_deleting_a_trashed_table_with_comments_cleans_up_the_rows(
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_getting_row_comments_executes_fixed_number_of_queries(
data_fixture, api_client, django_assert_num_queries
premium_data_fixture, api_client, django_assert_num_queries
):
user, token = data_fixture.create_user_and_token(first_name="Test User")
other_user, other_token = data_fixture.create_user_and_token(first_name="Test User")
user, token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
other_user, other_token = premium_data_fixture.create_user_and_token(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = data_fixture.build_table(
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row", "second row"], user=user
)
data_fixture.create_user_group(user=other_user, group=table.database.group)
premium_data_fixture.create_user_group(user=other_user, group=table.database.group)
response = api_client.post(
reverse(
@ -506,7 +609,7 @@ def test_getting_row_comments_executes_fixed_number_of_queries(
)
assert response.status_code == HTTP_200_OK
expected_num_of_fixed_queries = 6
expected_num_of_fixed_queries = 7
with django_assert_num_queries(expected_num_of_fixed_queries):
response = api_client.get(
reverse(

View file

@ -1,2 +1,15 @@
import pytest
# noinspection PyUnresolvedReferences
from tests.conftest import * # noqa: F403, F401
@pytest.fixture
def premium_data_fixture(data_fixture):
from .fixtures import PremiumFixtures
class PremiumFixtures(PremiumFixtures, data_fixture.__class__):
pass
return PremiumFixtures()

View file

@ -4,10 +4,14 @@ from unittest.mock import patch
import pytest
from django.utils.dateparse import parse_date, parse_datetime
from django.utils.timezone import utc, make_aware
from django.test.utils import override_settings
from baserow.contrib.database.export.handler import ExportHandler
from baserow.contrib.database.rows.handler import RowHandler
from tests.test_utils import setup_interesting_test_table
from baserow_premium.license.exceptions import NoPremiumLicenseError
from baserow.test_utils.helpers import setup_interesting_test_table
def _parse_datetime(datetime):
@ -19,12 +23,16 @@ def _parse_date(date):
@pytest.mark.django_db
@override_settings(DEBUG=True)
@patch("baserow.contrib.database.export.handler.default_storage")
def test_can_export_every_interesting_different_field_to_json(
storage_mock, data_fixture
storage_mock, premium_data_fixture
):
contents = run_export_over_interesting_test_table(
data_fixture, storage_mock, {"exporter_type": "json"}
premium_data_fixture,
storage_mock,
{"exporter_type": "json"},
user_kwargs={"has_active_premium_license": True},
)
assert (
contents
@ -128,16 +136,27 @@ def test_can_export_every_interesting_different_field_to_json(
@pytest.mark.django_db
@override_settings(DEBUG=True)
@patch("baserow.contrib.database.export.handler.default_storage")
def test_if_duplicate_field_names_json_export(storage_mock, data_fixture):
user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user)
table = data_fixture.create_database_table(database=database)
data_fixture.create_text_field(table=table, name="name", order=1)
data_fixture.create_text_field(table=table, name="name", order=2)
data_fixture.create_text_field(table=table, name="name", order=3)
data_fixture.create_text_field(table=table, name='Another"name', order=4)
data_fixture.create_text_field(table=table, name='Another"name', order=5)
def test_cannot_export_json_without_premium_license(storage_mock, premium_data_fixture):
with pytest.raises(NoPremiumLicenseError):
run_export_over_interesting_test_table(
premium_data_fixture, storage_mock, {"exporter_type": "json"}
)
@pytest.mark.django_db
@override_settings(DEBUG=True)
@patch("baserow.contrib.database.export.handler.default_storage")
def test_if_duplicate_field_names_json_export(storage_mock, premium_data_fixture):
user = premium_data_fixture.create_user(has_active_premium_license=True)
database = premium_data_fixture.create_database_application(user=user)
table = premium_data_fixture.create_database_table(database=database)
premium_data_fixture.create_text_field(table=table, name="name", order=1)
premium_data_fixture.create_text_field(table=table, name="name", order=2)
premium_data_fixture.create_text_field(table=table, name="name", order=3)
premium_data_fixture.create_text_field(table=table, name='Another"name', order=4)
premium_data_fixture.create_text_field(table=table, name='Another"name', order=5)
row_handler = RowHandler()
row_handler.create_row(user=user, table=table)
job, contents = run_export_job_with_mock_storage(
@ -160,12 +179,16 @@ def test_if_duplicate_field_names_json_export(storage_mock, data_fixture):
@pytest.mark.django_db
@override_settings(DEBUG=True)
@patch("baserow.contrib.database.export.handler.default_storage")
def test_can_export_every_interesting_different_field_to_xml(
storage_mock, data_fixture
storage_mock, premium_data_fixture
):
xml = run_export_over_interesting_test_table(
data_fixture, storage_mock, {"exporter_type": "xml"}
premium_data_fixture,
storage_mock,
{"exporter_type": "xml"},
user_kwargs={"has_active_premium_license": True},
)
expected_xml = f"""<?xml version="1.0" encoding="utf-8" ?>
<rows>
@ -270,18 +293,21 @@ def test_can_export_every_interesting_different_field_to_xml(
@pytest.mark.django_db
@override_settings(DEBUG=True)
@patch("baserow.contrib.database.export.handler.default_storage")
def test_if_xml_duplicate_name_and_value_are_escaped(storage_mock, data_fixture):
user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user)
table = data_fixture.create_database_table(database=database)
text = data_fixture.create_text_field(table=table, name="<name>", order=0)
data_fixture.create_text_field(table=table, name="name", order=1)
data_fixture.create_text_field(table=table, name="Another name", order=2)
data_fixture.create_text_field(table=table, name="Another@name", order=3)
empty_1 = data_fixture.create_text_field(table=table, name="@", order=4)
empty_2 = data_fixture.create_text_field(table=table, name="", order=5)
data_fixture.create_text_field(table=table, name="1", order=6)
def test_if_xml_duplicate_name_and_value_are_escaped(
storage_mock, premium_data_fixture
):
user = premium_data_fixture.create_user(has_active_premium_license=True)
database = premium_data_fixture.create_database_application(user=user)
table = premium_data_fixture.create_database_table(database=database)
text = premium_data_fixture.create_text_field(table=table, name="<name>", order=0)
premium_data_fixture.create_text_field(table=table, name="name", order=1)
premium_data_fixture.create_text_field(table=table, name="Another name", order=2)
premium_data_fixture.create_text_field(table=table, name="Another@name", order=3)
empty_1 = premium_data_fixture.create_text_field(table=table, name="@", order=4)
empty_2 = premium_data_fixture.create_text_field(table=table, name="", order=5)
premium_data_fixture.create_text_field(table=table, name="1", order=6)
row_handler = RowHandler()
row_handler.create_row(
user=user,
@ -310,13 +336,25 @@ def test_if_xml_duplicate_name_and_value_are_escaped(storage_mock, data_fixture)
)
@pytest.mark.django_db
@override_settings(DEBUG=True)
@patch("baserow.contrib.database.export.handler.default_storage")
def test_cannot_export_xml_without_premium_license(storage_mock, premium_data_fixture):
with pytest.raises(NoPremiumLicenseError):
run_export_over_interesting_test_table(
premium_data_fixture, storage_mock, {"exporter_type": "xml"}
)
def strip_indents_and_newlines(xml):
return "".join([line.strip() for line in xml.split("\n")])
def run_export_over_interesting_test_table(data_fixture, storage_mock, options):
table, user, _, _ = setup_interesting_test_table(data_fixture)
grid_view = data_fixture.create_grid_view(table=table)
def run_export_over_interesting_test_table(
premium_data_fixture, storage_mock, options, user_kwargs=None
):
table, user, _, _ = setup_interesting_test_table(premium_data_fixture, user_kwargs)
grid_view = premium_data_fixture.create_grid_view(table=table)
job, contents = run_export_job_with_mock_storage(
table, grid_view, storage_mock, user, options
)

View file

@ -0,0 +1,65 @@
from baserow_premium.license.models import License, LicenseUser
VALID_ONE_SEAT_LICENSE = (
# id: "1", instance_id: "1"
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogIjEiLCAidmFsaWRfZnJvbSI6ICIyMDIxLTA4LTI5VDE5OjUyOjU3"
b"Ljg0MjY5NiIsICJ2YWxpZF90aHJvdWdoIjogIjIwMjEtMDktMjlUMTk6NTI6NTcuODQyNjk2IiwgInBy"
b"b2R1Y3RfY29kZSI6ICJwcmVtaXVtIiwgInNlYXRzIjogMSwgImlzc3VlZF9vbiI6ICIyMDIxLTA4LTI5"
b"VDE5OjUyOjU3Ljg0MjY5NiIsICJpc3N1ZWRfdG9fZW1haWwiOiAiYnJhbUBiYXNlcm93LmlvIiwgImlz"
b"c3VlZF90b19uYW1lIjogIkJyYW0iLCAiaW5zdGFuY2VfaWQiOiAiMSJ9.e33Z4CxLSmD-R55Es24P3mR"
b"8Oqn3LpaXvgYLzF63oFHat3paon7IBjBmOX3eyd8KjirVf3empJds4uUw2Nn2m7TVvRAtJ8XzNl-8ytf"
b"2RLtmjMx1Xkgp5VZ8S7UqJ_cKLyl76eVRtGEA1DH2HdPKu1vBPJ4bzDfnhDPYl4k5z9XSSgqAbQ9WO0U"
b"5kiI3BYjVRZSKnZMeguAGZ47ezDj_WArGcHAB8Pa2v3HFp5Y34DMJ8r3_hD5hxCKgoNx4AHx1Q-hRDqp"
b"Aroj-4jl7KWvlP-OJNc1BgH2wnhFmeKHotv-Iumi83JQohyceUbG6j8rDDQvJfcn0W2_ebmUH3TKr-w="
b"="
)
VALID_100_SEAT_LICENSE_UNTIL_YEAR_2099 = (
# id: "test-license", instance_id: "1"
# valid from the year 1000 through the year 2099
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogInRlc3QtbGljZW5zZSIsICJ2YWxpZF9mcm9tIjogIjEwMDAtMDEt"
b"MDFUMTI6MDA6MDAuMDAwMDAwIiwgInZhbGlkX3Rocm91Z2giOiAiMjA5OS0wMS0wMVQxMjowMDowMC4w"
b"MDAwMDAiLCAicHJvZHVjdF9jb2RlIjogInByZW1pdW0iLCAic2VhdHMiOiAxMDAsICJpc3N1ZWRfb24i"
b"OiAiMjAyMS0wOC0yOVQxOTo1Mjo1Ny44NDI2OTYiLCAiaXNzdWVkX3RvX2VtYWlsIjogImJyYW1AYmFz"
b"ZXJvdy5pbyIsICJpc3N1ZWRfdG9fbmFtZSI6ICJCcmFtIiwgImluc3RhbmNlX2lkIjogIjEifQ==.SoF"
b"QKxwNjNM-lvJ4iy7d8dc4EmWZagMBzgAmQgUJoGo6FtXaTOILOnc3Tm2uSwZ2MImBeehIff8GPE521-k"
b"a9-0DDYEX4BYVgpLxLF3gFZxgX0QJgsNsauOjEZH8IGFGX1Asdsll2rNbzYDKz68jG7GgK04Lfn19cQ-"
b"Qg0Qlgq0gB_7CoUulo73fhCjOZHoH1HAzxh77SbgXxJbDQOYlXqortVvl5vDpNcPTbar4IihBJRgaFTM"
b"7DjR0On8GCX7j944VkXguiGPdglBXTcqRbPf1qqmZ8jaHrKX6wHYysBJs10OqWqT5p_s8cuRrr0IzLDz"
b"Ss-q11zuFn-ekeJzo5A=="
)
class PremiumFixtures:
def create_user(self, *args, **kwargs):
has_active_premium_license = kwargs.pop("has_active_premium_license", False)
user = super().create_user(*args, **kwargs)
if has_active_premium_license:
self.create_active_premium_license_for_user(user)
return user
def create_active_premium_license_for_user(self, user):
test_license, created = License.objects.get_or_create(
license=VALID_100_SEAT_LICENSE_UNTIL_YEAR_2099.decode()
)
LicenseUser.objects.get_or_create(user=user, license=test_license)
def remove_all_active_premium_licenses(self, user):
LicenseUser.objects.filter(user=user).delete()
def create_premium_license(self, **kwargs):
if "license" not in kwargs:
kwargs["license"] = VALID_100_SEAT_LICENSE_UNTIL_YEAR_2099.decode()
return License.objects.create(**kwargs)
def create_premium_license_user(self, **kwargs):
if "user" not in kwargs:
kwargs["user"] = self.create_user()
if "license" not in kwargs:
kwargs["license"] = self.create_premium_license()
return LicenseUser.objects.create(**kwargs)

View file

@ -0,0 +1,841 @@
import pytest
import responses
import base64
from freezegun import freeze_time
from unittest.mock import patch
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from django.db import transaction
from django.test.utils import override_settings
from baserow.core.exceptions import IsNotAdminError
from baserow_premium.license.handler import (
has_active_premium_license,
check_active_premium_license,
get_public_key,
decode_license,
fetch_license_status_with_authority,
check_licenses,
register_license,
remove_license,
add_user_to_license,
remove_user_from_license,
fill_remaining_seats_of_license,
remove_all_users_from_license,
)
from baserow_premium.license.models import License, LicenseUser
from baserow_premium.license.exceptions import (
NoPremiumLicenseError,
InvalidPremiumLicenseError,
UnsupportedPremiumLicenseError,
PremiumLicenseInstanceIdMismatchError,
PremiumLicenseHasExpired,
PremiumLicenseAlreadyExists,
NoSeatsLeftInPremiumLicenseError,
UserAlreadyOnPremiumLicenseError,
LicenseAuthorityUnavailable,
)
VALID_ONE_SEAT_LICENSE = (
# id: "1", instance_id: "1"
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogIjEiLCAidmFsaWRfZnJvbSI6ICIyMDIxLTA4LTI5VDE5OjUyOjU3"
b"Ljg0MjY5NiIsICJ2YWxpZF90aHJvdWdoIjogIjIwMjEtMDktMjlUMTk6NTI6NTcuODQyNjk2IiwgInBy"
b"b2R1Y3RfY29kZSI6ICJwcmVtaXVtIiwgInNlYXRzIjogMSwgImlzc3VlZF9vbiI6ICIyMDIxLTA4LTI5"
b"VDE5OjUyOjU3Ljg0MjY5NiIsICJpc3N1ZWRfdG9fZW1haWwiOiAiYnJhbUBiYXNlcm93LmlvIiwgImlz"
b"c3VlZF90b19uYW1lIjogIkJyYW0iLCAiaW5zdGFuY2VfaWQiOiAiMSJ9.e33Z4CxLSmD-R55Es24P3mR"
b"8Oqn3LpaXvgYLzF63oFHat3paon7IBjBmOX3eyd8KjirVf3empJds4uUw2Nn2m7TVvRAtJ8XzNl-8ytf"
b"2RLtmjMx1Xkgp5VZ8S7UqJ_cKLyl76eVRtGEA1DH2HdPKu1vBPJ4bzDfnhDPYl4k5z9XSSgqAbQ9WO0U"
b"5kiI3BYjVRZSKnZMeguAGZ47ezDj_WArGcHAB8Pa2v3HFp5Y34DMJ8r3_hD5hxCKgoNx4AHx1Q-hRDqp"
b"Aroj-4jl7KWvlP-OJNc1BgH2wnhFmeKHotv-Iumi83JQohyceUbG6j8rDDQvJfcn0W2_ebmUH3TKr-w="
b"="
)
VALID_UPGRADED_TEN_SEAT_LICENSE = (
# id: "1", instance_id: "1"
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogIjEiLCAidmFsaWRfZnJvbSI6ICIyMDIxLTA4LTI5VDE5OjUyOjU3"
b"Ljg0MjY5NiIsICJ2YWxpZF90aHJvdWdoIjogIjIwMjEtMDktMjlUMTk6NTI6NTcuODQyNjk2IiwgInBy"
b"b2R1Y3RfY29kZSI6ICJwcmVtaXVtIiwgInNlYXRzIjogMTAsICJpc3N1ZWRfb24iOiAiMjAyMS0wOC0z"
b"MFQxOTo1Mjo1Ny44NDI2OTYiLCAiaXNzdWVkX3RvX2VtYWlsIjogImJyYW1AYmFzZXJvdy5pbyIsICJp"
b"c3N1ZWRfdG9fbmFtZSI6ICJCcmFtIiwgImluc3RhbmNlX2lkIjogIjEifQ==.MLZn4TG1iZbXo1kjryk"
b"B98fFnYf8tOu8DG_I9CpkS5UGboI1-BNcq0pdtKxRgaTkRb-Q09D4J-LHKri5KA9WyQQNY8bb4antS1s"
b"svi8Yrp6p9VQhtCunKCqUuLA8mpHFNLV6nbsTKLds5imyFSMzT-8RLejT774RUQ3-DUYd2N-awbxBwDs"
b"Zpsupq3_7UrYIPhDcpVs_5G47p8ZT-z2fcC2cPOB2tRc6eQw7eUx95-nIxcR9IbLsHmQjYj3dxOjdsmN"
b"SDekPuwzbQiZnDpfy7kzc93-752AHTZ-O2gd83PFZziaIJSyu7mUsxWk4rkMQalO_XG9X0AOEraT0SQQ"
b"r0A=="
)
VALID_TWO_SEAT_LICENSE = (
# id: "2", instance_id: "1"
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogIjIiLCAidmFsaWRfZnJvbSI6ICIyMDIxLTA4LTI5VDE5OjUzOjM3"
b"LjA5MjMwMyIsICJ2YWxpZF90aHJvdWdoIjogIjIwMjEtMDktMjlUMTk6NTM6MzcuMDkyMzAzIiwgInBy"
b"b2R1Y3RfY29kZSI6ICJwcmVtaXVtIiwgInNlYXRzIjogMiwgImlzc3VlZF9vbiI6ICIyMDIxLTA4LTI5"
b"VDE5OjUzOjM3LjA5MjMwMyIsICJpc3N1ZWRfdG9fZW1haWwiOiAiYnJhbUBiYXNlcm93LmlvIiwgImlz"
b"c3VlZF90b19uYW1lIjogIkJyYW0iLCAiaW5zdGFuY2VfaWQiOiAiMSJ9.d41tB1kx69gw-9xDrRI0kER"
b"KDUtR-P6yRM3ufKZ_XRDewVCBAniCLe9-ce7TKabnMedE2cqHjYVLlI66Dfa5oH8fGswnyC16c9ZHlOU"
b"jQ5CpHTorZm6eyCXaP6MDdhstCNKdDrZns3qvVMAqDpmxS8wmiG9Y6gZjvBGXZWeoCraF1SVcUnFBBlf"
b"UemfGSQUwPitVlxJ6GWN-hzi7b1GZqWJKDb2YYJ0T30VMJeNO7oi6YHMUOH33041FU79DSET2A2NNEFu"
b"e-jnCcw5NFpH-zGzBDv1wpR3DFmJa78KwGbj0Kdzim85AUzi1xGRlIyxxTdTkVy2B-08lPaoG8Q62bw="
b"="
)
VALID_INSTANCE_TWO_LICENSE = (
# id: "2", instance_id: "2"
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogIjEiLCAidmFsaWRfZnJvbSI6ICIyMDIxLTA4LTI5VDE5OjUyOjU3"
b"Ljg0MjY5NiIsICJ2YWxpZF90aHJvdWdoIjogIjIwMjEtMDktMjlUMTk6NTI6NTcuODQyNjk2IiwgInBy"
b"b2R1Y3RfY29kZSI6ICJwcmVtaXVtIiwgInNlYXRzIjogMSwgImlzc3VlZF9vbiI6ICIyMDIxLTA4LTI5"
b"VDE5OjUyOjU3Ljg0MjY5NiIsICJpc3N1ZWRfdG9fZW1haWwiOiAiYnJhbUBiYXNlcm93LmlvIiwgImlz"
b"c3VlZF90b19uYW1lIjogIkJyYW0iLCAiaW5zdGFuY2VfaWQiOiAiMiJ9.i3Og4ZJwz__TxWyFc2B6lDi"
b"ZBAIOVTZv_jXVzQQqcjG-flPAicqXFECl7MbbexVmtsMES-U7VPebOh0t4oPoDXL1LiftfjmT63wO4An"
b"A3FMS0Ip0GIx2upkQC-MlU1kSR9Tltrr1qySuQvXORDRUaSxaRQQacwZTOIviVdcxG9vesjkFwn6LMYp"
b"-GhmCJXB0YfMgsvPm6kj6qTWPh3ed8aLNFnekUhB-dUwA4tqPicCQHRQCRZqzo9vx-hKdeHCGZMg0htG"
b"EB4cAeV4I29JXPC83qtwt6DSCPxudlJsli3tYsLMcxAHysVN3H_FAY8qg54MP33OKvZuwww5uFDITMQ="
b"="
)
INVALID_SIGNATURE_LICENSE = (
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogMSwgInZhbGlkX2Zyb20iOiAiMjAyMS0wOC0yOVQxOTo1NDoxMi4w"
b"NjY4NDYiLCAidmFsaWRfdGhyb3VnaCI6ICIyMDIxLTA5LTI5VDE5OjU0OjEyLjA2Njg0NiIsICJwcm9k"
b"dWN0X2NvZGUiOiAicHJlbWl1bSIsICJzZWF0cyI6IDEsICJ0b19lbWFpbCI6ICJicmFtQGJhc2Vyb3cu"
b"aW8iLCAidG9fbmFtZSI6ICJCcmFtIn0=.hYaWGO0M6s1pA9bhcBlk1fE1QMrhlDGNiBIBG_2O2AMGFPj"
b"gnsHdwfUIe_eo6dyAyvsToxBrpxr6N1vRqPdA61cKjTlhUdFqvj7NTeydS4Z9TlfP-vFslQk9CO_ok7Z"
b"ws8AHTQ2pKfsdzqcWNZnWKZeQGEtO73MIoFJbHr07mtWA1ZZgJNBTBpp-7BNtvj2bQyUeXyRKD5LVj8G"
b"ESDcapZCNt5ufesbYvpfs1c6p6UP4z3gszOYrzMApMqWHty7j10SDjcLIEsUTd02r_Pbip-KxmGfecXg"
b"B0HF7HJZwkY9ZdlZ7ODGtV0e455dQwh5sSHa3RRd71AXVou-cuOS87g=="
)
INVALID_PAYLOAD_LICENSE = (
b"e30=.gtDuoJAHn-LTPX1ReoGo8cm3DsXq0mf9MwpIccwCQXucpnh-r6yeJzRGbx5F80OGKXZJ1XcxLRr"
b"8-IssyxlGVcrhHt6iYXmNoPXUrxN1slOzMO4_tutvEHSuOntW5gctm9SFfcRrdbejYue_47brp779bP2"
b"pzwejOdQbSLUeNQ4bHIKQJYZ4cCooW8yICz6a8m4NFRDu_gr0Y1ud1Eo3h2E_BL2upNg14v8BRZJCHpj"
b"CC5Eg4ErKqm88iFStIEpub-vem9rEwKR2kIvdJ6DaD7AJTG507GEtbI9lNCkm2aPJSf142Rf8_NrTVh3"
b"QBqZnCo-XrquQe1h4r3fvjAf5tQ=="
)
INVALID_VERSION_LICENSE = (
b"eyJ2ZXJzaW9uIjogOTk5OX0=.rzAyL6qBkz_Eb3GYaSOXy9CJ2HJg4uAxtrbinh4aDYy7Eq4e4RpfaPm"
b"4dZLocIRxSmx_wUYSI0CMqmkwABHgzxRVmzVAmXf5MxX7vAGjjEnQX_dQOl8kY15gXhEQZv5pjSPVcZW"
b"CLll95OFoBUJhtOQqNC6JLA1LZdiSPG6zFhvi5V27sRGBz3E8jhFLWY-Y2WIq5_9q2d_hVFM0KHwRcxb"
b"CVof8RBUq1DgMcDKEGE7WRHYDVP1QugBjf4GZlvIE4ZVr3tKr0aKPX8nuNVhbQeudCW8tnturmxevpRN"
b"vLS5ETSQzJoP46cGuw0HUV20P4SnvQP_NRd5zifgllJqsUw=="
)
NOT_JSON_PAYLOAD_LICENSE = (
b"dGVzdA==.I37aSmuKN0WSrw6IDTg2xBOlQ3UOE5cjaWfc4MF5pgIadMUjkOh0D32R7RqRhmsxhdsqK6a"
b"bU8u8cT6ZG0PxjsRnFjrkbhdcFR1Yw9fHQ7plHShnpsj0NT8fMuDaVfCibKxyi-Z8nVmwHEIlyRkLfKV"
b"NMTR7q2bzdM9-LZ-jJsgp4qqtSE8ct8YwwdwUS8clKzb-wVyCDeGD2kBRyxNRU_hhiwN_aDv6zEEqd6u"
b"1lkIxotWs8hHJ3kT9EB9LY9Nb5qlm9Qt4bJY9OB4Bc8eEpXgEXGMUik11sTo5E3thoV6HJTUQWLwozbo"
b"fXwhO9qsjxisGZPEFinezHN124jSWxQ=="
)
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_has_active_premium_license(data_fixture):
user_in_license = data_fixture.create_user()
second_user_in_license = data_fixture.create_user()
user_not_in_license = data_fixture.create_user()
license = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
License.objects.create(license=VALID_ONE_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license, user=user_in_license)
license_user_2 = LicenseUser.objects.create(
license=license, user=second_user_in_license
)
with freeze_time("2021-08-01 12:00"):
assert not has_active_premium_license(user_in_license)
assert not has_active_premium_license(second_user_in_license)
assert not has_active_premium_license(user_not_in_license)
with freeze_time("2021-09-01 12:00"):
assert has_active_premium_license(user_in_license)
assert has_active_premium_license(second_user_in_license)
assert not has_active_premium_license(user_not_in_license)
with freeze_time("2021-10-01 12:00"):
assert not has_active_premium_license(user_in_license)
assert not has_active_premium_license(second_user_in_license)
assert not has_active_premium_license(user_not_in_license)
license_user_2.delete()
with freeze_time("2021-09-01 12:00"):
assert has_active_premium_license(user_in_license)
assert not has_active_premium_license(second_user_in_license)
assert not has_active_premium_license(user_not_in_license)
check_active_premium_license(user_in_license)
with pytest.raises(NoPremiumLicenseError):
check_active_premium_license(second_user_in_license)
with pytest.raises(NoPremiumLicenseError):
check_active_premium_license(user_not_in_license)
# When the license can't be decoded, it should also return false.
invalid_user = data_fixture.create_user()
invalid_license = License.objects.create(license="invalid")
LicenseUser.objects.create(license=invalid_license, user=invalid_user)
assert not has_active_premium_license(invalid_user)
@override_settings(DEBUG=True)
def test_get_public_key_debug():
public_key = get_public_key()
signature = base64.b64decode(
b"UnRzVNbgO8XxAHEjn6uzGrjdVjwf5rU2BcOe+G2nKHhF50m8nf/DAmmk6rsCFolrCXke2tJFnER"
b"0aeoPKwjZItnYJhkX0xt1PwkpImBoSZYQfdGycVuLwRv28yQaWP1tGonNIqpUuAiyuTrTEOWPid"
b"vbaYtAXu/I9aRwBSpjD3cM8mvyb4BE/lsC6RC1qYj6V2vUmoWum8sCQLHcToAs75CjV8NVVH97X"
b"TUnUellH3s+UpwHL9Rauq8rnPdAWLf6wujcqeBtdtsjp4HakuTsNYK+AcceSGeGSrlVqD0OoQei"
b"Cc2d0/5SkO3DyndZ/X73eX2psYpyd0p1ZDkCSbKJpA=="
)
# We do not expect the `InvalidSignature` exception.
assert (
public_key.verify(
signature,
b"test",
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256(),
)
is None
)
@override_settings(DEBUG=False)
def test_get_public_key_production():
public_key = get_public_key()
signature = base64.b64decode(
b"UnRzVNbgO8XxAHEjn6uzGrjdVjwf5rU2BcOe+G2nKHhF50m8nf/DAmmk6rsCFolrCXke2tJFnER"
b"0aeoPKwjZItnYJhkX0xt1PwkpImBoSZYQfdGycVuLwRv28yQaWP1tGonNIqpUuAiyuTrTEOWPid"
b"vbaYtAXu/I9aRwBSpjD3cM8mvyb4BE/lsC6RC1qYj6V2vUmoWum8sCQLHcToAs75CjV8NVVH97X"
b"TUnUellH3s+UpwHL9Rauq8rnPdAWLf6wujcqeBtdtsjp4HakuTsNYK+AcceSGeGSrlVqD0OoQei"
b"Cc2d0/5SkO3DyndZ/X73eX2psYpyd0p1ZDkCSbKJpA=="
)
# We expect the `InvalidSignature` exception because the signature has been
# signed with the wrong private key. This way, we know the debug key is not used
# in production.
with pytest.raises(InvalidSignature):
public_key.verify(
signature,
b"test",
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256(),
)
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_fetch_license_status_with_authority_unavailable(data_fixture):
data_fixture.update_settings(instance_id="1")
with pytest.raises(LicenseAuthorityUnavailable):
fetch_license_status_with_authority(["test"])
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
json={"error": "error"},
status=400,
)
with pytest.raises(LicenseAuthorityUnavailable):
fetch_license_status_with_authority(["test"])
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
body="not_json",
status=200,
)
with pytest.raises(LicenseAuthorityUnavailable):
fetch_license_status_with_authority(["test"])
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_fetch_license_status_with_authority_invalid_response(data_fixture):
data_fixture.update_settings(instance_id="1")
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
body="not_json",
status=200,
)
with pytest.raises(LicenseAuthorityUnavailable):
fetch_license_status_with_authority(["test"])
@pytest.mark.django_db
@override_settings(DEBUG=False)
@responses.activate
def test_fetch_license_status_in_production_mode(data_fixture):
data_fixture.update_settings(instance_id="1")
responses.add(
responses.POST,
"https://api.baserow.io/api/saas/licenses/check/",
json={"success": True},
status=200,
)
response = fetch_license_status_with_authority(["test"])
assert response["success"] is True
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_fetch_license_status_with_authority(data_fixture):
data_fixture.update_settings(instance_id="1")
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
json={"test": {"type": "ok", "detail": ""}},
status=200,
)
response = fetch_license_status_with_authority(["test"])
assert len(response) == 1
assert response["test"]["type"] == "ok"
@pytest.mark.django_db
@override_settings(DEBUG=True)
# Activate the responses because we want to check with the authority to fail.
@responses.activate
def test_check_licenses_with_authority_check(premium_data_fixture):
invalid_license = premium_data_fixture.create_premium_license(license="invalid")
does_not_exist_license = premium_data_fixture.create_premium_license(
license="does_not_exist"
)
instance_id_mismatch_license = premium_data_fixture.create_premium_license(
license="instance_id_mismatch"
)
updated_license = premium_data_fixture.create_premium_license(license="update")
ok_license = premium_data_fixture.create_premium_license(
license=VALID_TWO_SEAT_LICENSE.decode()
)
with freeze_time("2021-07-01 12:00"):
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
json={
"invalid": {"type": "invalid", "detail": ""},
"does_not_exist": {"type": "does_not_exist", "detail": ""},
"instance_id_mismatch": {
"type": "instance_id_mismatch",
"detail": "",
},
"update": {
"type": "update",
"detail": "",
"new_license_payload": VALID_ONE_SEAT_LICENSE.decode(),
},
VALID_TWO_SEAT_LICENSE.decode(): {"type": "ok", "detail": ""},
},
status=200,
)
check_licenses(
[
invalid_license,
does_not_exist_license,
instance_id_mismatch_license,
updated_license,
ok_license,
]
)
all_licenses = License.objects.all().order_by("id")
assert len(all_licenses) == 2
assert all_licenses[0].id == updated_license.id
assert all_licenses[0].license == VALID_ONE_SEAT_LICENSE.decode()
assert all_licenses[0].license_id == "1"
assert all_licenses[0].last_check.year == 2021
assert all_licenses[1].id == ok_license.id
assert all_licenses[1].last_check.year == 2021
@pytest.mark.django_db
@override_settings(DEBUG=True)
# Activate the responses because we want to check with the authority to fail.
@responses.activate
def test_check_licenses_without_authority_check(premium_data_fixture):
with freeze_time("2021-07-01 12:00"):
license_object = premium_data_fixture.create_premium_license(
license=VALID_TWO_SEAT_LICENSE.decode()
)
premium_data_fixture.create_premium_license_user(license=license_object)
premium_data_fixture.create_premium_license_user(license=license_object)
premium_data_fixture.create_premium_license_user(license=license_object)
premium_data_fixture.create_premium_license_user(license=license_object)
# This license is expected to be delete because the payload is invalid.
license_object_2 = premium_data_fixture.create_premium_license(
license="invalid_license"
)
assert License.objects.all().count() == 2
assert license_object.users.all().count() == 4
check_licenses([license_object, license_object_2])
assert License.objects.all().count() == 1
assert license_object.users.all().count() == 2
assert license_object.last_check.year == 2021
@override_settings(DEBUG=True)
def test_decode_license_with_valid_license():
assert decode_license(VALID_ONE_SEAT_LICENSE) == {
"version": 1,
"id": "1",
"valid_from": "2021-08-29T19:52:57.842696",
"valid_through": "2021-09-29T19:52:57.842696",
"product_code": "premium",
"seats": 1,
"issued_on": "2021-08-29T19:52:57.842696",
"issued_to_email": "bram@baserow.io",
"issued_to_name": "Bram",
"instance_id": "1",
}
assert decode_license(VALID_UPGRADED_TEN_SEAT_LICENSE) == {
"version": 1,
"id": "1",
"valid_from": "2021-08-29T19:52:57.842696",
"valid_through": "2021-09-29T19:52:57.842696",
"product_code": "premium",
"seats": 10,
"issued_on": "2021-08-30T19:52:57.842696",
"issued_to_email": "bram@baserow.io",
"issued_to_name": "Bram",
"instance_id": "1",
}
assert decode_license(VALID_TWO_SEAT_LICENSE) == {
"version": 1,
"id": "2",
"valid_from": "2021-08-29T19:53:37.092303",
"valid_through": "2021-09-29T19:53:37.092303",
"product_code": "premium",
"seats": 2,
"issued_on": "2021-08-29T19:53:37.092303",
"issued_to_email": "bram@baserow.io",
"issued_to_name": "Bram",
"instance_id": "1",
}
assert decode_license(VALID_INSTANCE_TWO_LICENSE) == {
"version": 1,
"id": "1",
"valid_from": "2021-08-29T19:52:57.842696",
"valid_through": "2021-09-29T19:52:57.842696",
"product_code": "premium",
"seats": 1,
"issued_on": "2021-08-29T19:52:57.842696",
"issued_to_email": "bram@baserow.io",
"issued_to_name": "Bram",
"instance_id": "2",
}
@override_settings(DEBUG=True)
def test_invalid_signature_decode_license():
with pytest.raises(InvalidPremiumLicenseError):
decode_license(INVALID_SIGNATURE_LICENSE)
with pytest.raises(InvalidPremiumLicenseError):
decode_license(INVALID_PAYLOAD_LICENSE)
with pytest.raises(InvalidPremiumLicenseError):
decode_license(b"test")
with pytest.raises(InvalidPremiumLicenseError):
decode_license(b"test.test")
with pytest.raises(InvalidPremiumLicenseError):
decode_license(b"test.test==")
with pytest.raises(InvalidPremiumLicenseError):
decode_license(b"eyJ2ZXJzaW9uIjog.rzAyL6qBkz_Eb==")
with pytest.raises(InvalidPremiumLicenseError):
decode_license(NOT_JSON_PAYLOAD_LICENSE)
@override_settings(DEBUG=True)
def test_unsupported_version_decode_license():
with pytest.raises(UnsupportedPremiumLicenseError):
decode_license(INVALID_VERSION_LICENSE)
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_register_license_with_authority_check_ok(data_fixture):
data_fixture.update_settings(instance_id="1")
admin_user = data_fixture.create_user(is_staff=True)
with freeze_time("2021-07-01 12:00"):
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
json={VALID_ONE_SEAT_LICENSE.decode(): {"type": "ok", "detail": ""}},
status=200,
)
license_1 = register_license(admin_user, VALID_ONE_SEAT_LICENSE)
assert license_1.license == VALID_ONE_SEAT_LICENSE.decode()
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_register_license_with_authority_check_updated(data_fixture):
data_fixture.update_settings(instance_id="1")
admin_user = data_fixture.create_user(is_staff=True)
with freeze_time("2021-07-01 12:00"):
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
json={
VALID_ONE_SEAT_LICENSE.decode(): {
"type": "update",
"detail": "",
"new_license_payload": VALID_UPGRADED_TEN_SEAT_LICENSE.decode(),
}
},
status=200,
)
license_1 = register_license(admin_user, VALID_ONE_SEAT_LICENSE)
assert license_1.license == VALID_UPGRADED_TEN_SEAT_LICENSE.decode()
assert license_1.license_id == "1"
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_register_license_with_authority_check_does_not_exist(data_fixture):
data_fixture.update_settings(instance_id="1")
admin_user = data_fixture.create_user(is_staff=True)
with freeze_time("2021-07-01 12:00"):
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
json={
VALID_ONE_SEAT_LICENSE.decode(): {
"type": "does_not_exist",
"detail": "",
}
},
status=200,
)
with pytest.raises(InvalidPremiumLicenseError):
register_license(admin_user, VALID_ONE_SEAT_LICENSE)
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_register_license_with_authority_check_instance_id_mismatch(data_fixture):
data_fixture.update_settings(instance_id="1")
admin_user = data_fixture.create_user(is_staff=True)
with freeze_time("2021-07-01 12:00"):
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
json={
VALID_ONE_SEAT_LICENSE.decode(): {
"type": "instance_id_mismatch",
"detail": "",
}
},
status=200,
)
with pytest.raises(PremiumLicenseInstanceIdMismatchError):
register_license(admin_user, VALID_ONE_SEAT_LICENSE)
@pytest.mark.django_db
@override_settings(DEBUG=True)
@responses.activate
def test_register_license_with_authority_check_invalid(data_fixture):
data_fixture.update_settings(instance_id="1")
admin_user = data_fixture.create_user(is_staff=True)
with freeze_time("2021-07-01 12:00"):
responses.add(
responses.POST,
"http://172.17.0.1:8001/api/saas/licenses/check/",
json={
VALID_ONE_SEAT_LICENSE.decode(): {
"type": "invalid",
"detail": "",
}
},
status=200,
)
with pytest.raises(InvalidPremiumLicenseError):
register_license(admin_user, VALID_ONE_SEAT_LICENSE)
@pytest.mark.django_db
@override_settings(DEBUG=True)
# We need to activate the responses here, so that the license authority check will
# fail and therefore be ignored. There is another test, that tests the authority
# responses.
@responses.activate
def test_register_license(data_fixture):
data_fixture.update_settings(instance_id="1")
normal_user = data_fixture.create_user()
admin_user = data_fixture.create_user(is_staff=True)
with pytest.raises(IsNotAdminError):
register_license(normal_user, VALID_ONE_SEAT_LICENSE)
with freeze_time("2021-10-01 12:00"):
with pytest.raises(PremiumLicenseHasExpired):
register_license(admin_user, VALID_ONE_SEAT_LICENSE)
with freeze_time("2021-07-01 12:00"):
license_1 = register_license(admin_user, VALID_ONE_SEAT_LICENSE)
assert license_1.license == VALID_ONE_SEAT_LICENSE.decode()
# Check if the license has actually been created.
all_licenses = License.objects.all()
assert len(all_licenses) == 1
assert all_licenses[0].id == license_1.id
with freeze_time("2021-09-01 12:00"):
with pytest.raises(PremiumLicenseInstanceIdMismatchError):
register_license(admin_user, VALID_INSTANCE_TWO_LICENSE)
with pytest.raises(PremiumLicenseAlreadyExists):
register_license(admin_user, VALID_ONE_SEAT_LICENSE)
license_2 = register_license(admin_user, VALID_TWO_SEAT_LICENSE.decode())
assert license_2.license == VALID_TWO_SEAT_LICENSE.decode()
@pytest.mark.django_db
@override_settings(DEBUG=True)
# We need to activate the responses here, so that the license authority check will
# fail and therefore be ignored. There is another test, that tests the authority
# responses.
@responses.activate
def test_upgrade_license_by_register(data_fixture):
data_fixture.update_settings(instance_id="1")
admin_user = data_fixture.create_user(is_staff=True)
with freeze_time("2021-07-01 12:00"):
first_license_1 = register_license(admin_user, VALID_ONE_SEAT_LICENSE)
second_license_1 = register_license(admin_user, VALID_UPGRADED_TEN_SEAT_LICENSE)
assert first_license_1.id == second_license_1.id
assert License.objects.all().count() == 1
assert second_license_1.license_id == "1"
assert second_license_1.seats == 10
@pytest.mark.django_db
@override_settings(DEBUG=True)
# We need to activate the responses here, so that the license authority check will
# fail and therefore be ignored. There is another test, that tests the authority
# responses.
@responses.activate
def test_register_an_older_license(data_fixture):
data_fixture.update_settings(instance_id="1")
admin_user = data_fixture.create_user(is_staff=True)
with freeze_time("2021-07-01 12:00"):
register_license(admin_user, VALID_UPGRADED_TEN_SEAT_LICENSE)
# The same license already exists.
with pytest.raises(PremiumLicenseAlreadyExists):
register_license(admin_user, VALID_UPGRADED_TEN_SEAT_LICENSE)
# An older license already exists.
with pytest.raises(PremiumLicenseAlreadyExists):
register_license(admin_user, VALID_ONE_SEAT_LICENSE)
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_remove_license(data_fixture):
user_1 = data_fixture.create_user()
user_2 = data_fixture.create_user()
admin_1 = data_fixture.create_user(is_staff=True)
license_object = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license_object, user=user_1)
license_object_2 = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license_object_2, user=user_1)
LicenseUser.objects.create(license=license_object_2, user=user_2)
with pytest.raises(IsNotAdminError):
remove_license(user_1, license_object)
remove_license(admin_1, license_object_2)
licenses = License.objects.all()
assert len(licenses) == 1
assert licenses[0].id == license_object.id
license_users = LicenseUser.objects.all()
assert len(license_users) == 1
assert license_users[0].user_id == user_1.id
assert license_users[0].license_id == license_object.id
@pytest.mark.django_db(transaction=True)
@override_settings(DEBUG=True)
@patch("baserow_premium.license.handler.broadcast_to_users")
def test_add_user_to_license(mock_broadcast_to_users, data_fixture):
with freeze_time("2021-09-01 12:00"):
user_1 = data_fixture.create_user()
user_2 = data_fixture.create_user()
admin_1 = data_fixture.create_user(is_staff=True)
license_object = License.objects.create(license=VALID_ONE_SEAT_LICENSE.decode())
with pytest.raises(IsNotAdminError):
add_user_to_license(user_1, license_object, user_1)
license_user = add_user_to_license(admin_1, license_object, user_1)
assert license_user.user_id == user_1.id
assert license_user.license_id == license_object.id
mock_broadcast_to_users.delay.assert_called_once()
args = mock_broadcast_to_users.delay.call_args
assert args[0][0] == [user_1.id]
assert args[0][1]["type"] == "user_data_updated"
assert args[0][1]["user_data"] == {"premium": {"valid_license": True}}
with pytest.raises(UserAlreadyOnPremiumLicenseError):
add_user_to_license(admin_1, license_object, user_1)
with pytest.raises(NoSeatsLeftInPremiumLicenseError):
add_user_to_license(admin_1, license_object, user_2)
@pytest.mark.django_db(transaction=True)
@override_settings(DEBUG=True)
@patch("baserow_premium.license.handler.broadcast_to_users")
def test_remove_user_from_license(mock_broadcast_to_users, data_fixture):
with freeze_time("2021-09-01 12:00"):
user_1 = data_fixture.create_user()
admin_1 = data_fixture.create_user(is_staff=True)
license_object = License.objects.create(license=VALID_ONE_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license_object, user=user_1)
with pytest.raises(IsNotAdminError):
remove_user_from_license(user_1, license_object, user_1)
with transaction.atomic():
remove_user_from_license(admin_1, license_object, user_1)
assert LicenseUser.objects.all().count() == 0
mock_broadcast_to_users.delay.assert_called_once()
args = mock_broadcast_to_users.delay.call_args
assert args[0][0] == [user_1.id]
assert args[0][1]["type"] == "user_data_updated"
assert args[0][1]["user_data"] == {"premium": {"valid_license": False}}
@pytest.mark.django_db(transaction=True)
@override_settings(DEBUG=True)
@patch("baserow_premium.license.handler.broadcast_to_users")
def test_fill_remaining_seats_in_license(mock_broadcast_to_users, data_fixture):
with freeze_time("2021-09-01 12:00"):
user_1 = data_fixture.create_user()
user_2 = data_fixture.create_user()
data_fixture.create_user()
admin_1 = data_fixture.create_user(is_staff=True)
license_object = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license_object, user=user_1)
with pytest.raises(IsNotAdminError):
fill_remaining_seats_of_license(user_1, license_object)
fill_remaining_seats_of_license(admin_1, license_object)
license_users = LicenseUser.objects.filter(license=license_object).order_by(
"user_id"
)
assert len(license_users) == 2
assert license_users[0].license_id == license_object.id
assert license_users[0].user_id == user_1.id
assert license_users[1].license_id == license_object.id
assert license_users[1].user_id == user_2.id
mock_broadcast_to_users.delay.assert_called_once()
args = mock_broadcast_to_users.delay.call_args
assert len(args[0][0]) == 1
assert args[0][0][0] == user_2.id
assert args[0][1]["type"] == "user_data_updated"
assert args[0][1]["user_data"] == {"premium": {"valid_license": True}}
license_object_2 = License.objects.create(
license=VALID_ONE_SEAT_LICENSE.decode()
)
created_license_users = fill_remaining_seats_of_license(
admin_1, license_object_2
)
assert len(created_license_users) == 1
assert created_license_users[0].license_id == license_object_2.id
assert created_license_users[0].user_id == user_1.id
assert LicenseUser.objects.all().count() == 3
license_users = LicenseUser.objects.filter(license=license_object_2).order_by(
"user_id"
)
assert len(license_users) == 1
assert license_users[0].license_id == license_object_2.id
assert license_users[0].user_id == user_1.id
@pytest.mark.django_db(transaction=True)
@override_settings(DEBUG=True)
@patch("baserow_premium.license.handler.broadcast_to_users")
def test_remove_all_users_from_license(mock_broadcast_to_users, data_fixture):
with freeze_time("2021-09-01 12:00"):
user_1 = data_fixture.create_user()
user_2 = data_fixture.create_user()
admin_1 = data_fixture.create_user(is_staff=True)
license_object = License.objects.create(license=VALID_TWO_SEAT_LICENSE.decode())
LicenseUser.objects.create(license=license_object, user=user_1)
license_object_2 = License.objects.create(
license=VALID_TWO_SEAT_LICENSE.decode()
)
LicenseUser.objects.create(license=license_object_2, user=user_1)
LicenseUser.objects.create(license=license_object_2, user=user_2)
with pytest.raises(IsNotAdminError):
remove_all_users_from_license(user_1, license_object)
remove_all_users_from_license(admin_1, license_object_2)
license_users = LicenseUser.objects.all()
assert len(license_users) == 1
assert license_users[0].license_id == license_object.id
assert license_users[0].user_id == user_1.id
assert LicenseUser.objects.all().count() == 1
mock_broadcast_to_users.delay.assert_called_once()
args = mock_broadcast_to_users.delay.call_args
assert len(args[0][0]) == 2
assert user_1.id in args[0][0]
assert user_2.id in args[0][0]
assert args[0][1]["type"] == "user_data_updated"
assert args[0][1]["user_data"] == {"premium": {"valid_license": False}}

View file

@ -0,0 +1,91 @@
import pytest
from datetime import datetime
from freezegun import freeze_time
from django.test.utils import override_settings
from django.utils.timezone import utc, make_aware
from baserow_premium.license.models import License
VALID_ONE_SEAT_LICENSE = (
# id: "1", instance_id: "1"
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogIjEiLCAidmFsaWRfZnJvbSI6ICIyMDIxLTA4LTI5VDE5OjUyOjU3"
b"Ljg0MjY5NiIsICJ2YWxpZF90aHJvdWdoIjogIjIwMjEtMDktMjlUMTk6NTI6NTcuODQyNjk2IiwgInBy"
b"b2R1Y3RfY29kZSI6ICJwcmVtaXVtIiwgInNlYXRzIjogMSwgImlzc3VlZF9vbiI6ICIyMDIxLTA4LTI5"
b"VDE5OjUyOjU3Ljg0MjY5NiIsICJpc3N1ZWRfdG9fZW1haWwiOiAiYnJhbUBiYXNlcm93LmlvIiwgImlz"
b"c3VlZF90b19uYW1lIjogIkJyYW0iLCAiaW5zdGFuY2VfaWQiOiAiMSJ9.e33Z4CxLSmD-R55Es24P3mR"
b"8Oqn3LpaXvgYLzF63oFHat3paon7IBjBmOX3eyd8KjirVf3empJds4uUw2Nn2m7TVvRAtJ8XzNl-8ytf"
b"2RLtmjMx1Xkgp5VZ8S7UqJ_cKLyl76eVRtGEA1DH2HdPKu1vBPJ4bzDfnhDPYl4k5z9XSSgqAbQ9WO0U"
b"5kiI3BYjVRZSKnZMeguAGZ47ezDj_WArGcHAB8Pa2v3HFp5Y34DMJ8r3_hD5hxCKgoNx4AHx1Q-hRDqp"
b"Aroj-4jl7KWvlP-OJNc1BgH2wnhFmeKHotv-Iumi83JQohyceUbG6j8rDDQvJfcn0W2_ebmUH3TKr-w="
b"="
)
VALID_TWO_SEAT_LICENSE = (
# id: "2", instance_id: "1"
b"eyJ2ZXJzaW9uIjogMSwgImlkIjogIjIiLCAidmFsaWRfZnJvbSI6ICIyMDIxLTA4LTI5VDE5OjUzOjM3"
b"LjA5MjMwMyIsICJ2YWxpZF90aHJvdWdoIjogIjIwMjEtMDktMjlUMTk6NTM6MzcuMDkyMzAzIiwgInBy"
b"b2R1Y3RfY29kZSI6ICJwcmVtaXVtIiwgInNlYXRzIjogMiwgImlzc3VlZF9vbiI6ICIyMDIxLTA4LTI5"
b"VDE5OjUzOjM3LjA5MjMwMyIsICJpc3N1ZWRfdG9fZW1haWwiOiAiYnJhbUBiYXNlcm93LmlvIiwgImlz"
b"c3VlZF90b19uYW1lIjogIkJyYW0iLCAiaW5zdGFuY2VfaWQiOiAiMSJ9.d41tB1kx69gw-9xDrRI0kER"
b"KDUtR-P6yRM3ufKZ_XRDewVCBAniCLe9-ce7TKabnMedE2cqHjYVLlI66Dfa5oH8fGswnyC16c9ZHlOU"
b"jQ5CpHTorZm6eyCXaP6MDdhstCNKdDrZns3qvVMAqDpmxS8wmiG9Y6gZjvBGXZWeoCraF1SVcUnFBBlf"
b"UemfGSQUwPitVlxJ6GWN-hzi7b1GZqWJKDb2YYJ0T30VMJeNO7oi6YHMUOH33041FU79DSET2A2NNEFu"
b"e-jnCcw5NFpH-zGzBDv1wpR3DFmJa78KwGbj0Kdzim85AUzi1xGRlIyxxTdTkVy2B-08lPaoG8Q62bw="
b"="
)
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_premium_license_model_properties():
license = License(license=VALID_ONE_SEAT_LICENSE.decode())
assert license.license_id == "1"
assert license.valid_from == make_aware(
datetime(2021, 8, 29, 19, 52, 57, 842696), utc
)
assert license.valid_through == make_aware(
datetime(2021, 9, 29, 19, 52, 57, 842696), utc
)
assert license.product_code == "premium"
assert license.seats == 1
assert license.issued_on == make_aware(
datetime(2021, 8, 29, 19, 52, 57, 842696), utc
)
assert license.issued_to_email == "bram@baserow.io"
assert license.issued_to_name == "Bram"
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_premium_license_model_is_active():
license = License(license=VALID_ONE_SEAT_LICENSE.decode())
with freeze_time("2021-08-29 19:50"):
assert not license.is_active
with freeze_time("2021-08-29 20:00"):
assert license.is_active
with freeze_time("2021-09-29 19:50"):
assert license.is_active
with freeze_time("2021-09-29 19:54"):
assert not license.is_active
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_premium_license_model_save():
license_1 = License(license=VALID_ONE_SEAT_LICENSE.decode())
license_2 = License(license=VALID_TWO_SEAT_LICENSE.decode())
assert license_1.license_id == "1"
license_1.license = VALID_TWO_SEAT_LICENSE.decode()
license_1.save()
license_2.license = VALID_ONE_SEAT_LICENSE.decode()
license_2.save()
assert license_1.license_id == "2"
assert license_2.license_id == "1"

View file

@ -0,0 +1,18 @@
import pytest
from unittest.mock import patch
from baserow_premium.license.tasks import license_check
@pytest.mark.django_db
@patch("baserow_premium.license.handler.check_licenses")
def test_license_check(mock_check_licenses, premium_data_fixture):
license_check()
assert not mock_check_licenses.called
premium_data_fixture.create_premium_license()
license_check()
assert mock_check_licenses.called

View file

@ -3,15 +3,21 @@ from unittest.mock import patch, call
import pytest
from freezegun import freeze_time
from django.test.utils import override_settings
from baserow.contrib.database.rows.handler import RowHandler
from baserow_premium.row_comments.exceptions import InvalidRowCommentException
from baserow_premium.row_comments.handler import RowCommentHandler
from baserow_premium.license.exceptions import NoPremiumLicenseError
@pytest.mark.django_db
def test_cant_make_null_comment_using_handler(data_fixture):
user = data_fixture.create_user(first_name="Test User")
table, fields, rows = data_fixture.build_table(
@override_settings(DEBUG=True)
def test_cant_make_null_comment_using_handler(premium_data_fixture):
user = premium_data_fixture.create_user(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row", "second_row"], user=user
)
with pytest.raises(InvalidRowCommentException):
@ -20,20 +26,39 @@ def test_cant_make_null_comment_using_handler(data_fixture):
@pytest.mark.django_db
def test_cant_make_blank_comment_using_handler(data_fixture):
user = data_fixture.create_user(first_name="Test User")
table, fields, rows = data_fixture.build_table(
@override_settings(DEBUG=True)
def test_cant_make_blank_comment_using_handler(premium_data_fixture):
user = premium_data_fixture.create_user(
first_name="Test User", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row", "second_row"], user=user
)
with pytest.raises(InvalidRowCommentException):
RowCommentHandler.create_comment(user, table.id, rows[0].id, "")
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_cant_create_comment_without_premium_license(premium_data_fixture):
user = premium_data_fixture.create_user(first_name="Test User")
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row", "second_row"], user=user
)
with pytest.raises(NoPremiumLicenseError):
RowCommentHandler.create_comment(user, table.id, rows[0].id, "Test")
@pytest.mark.django_db(transaction=True)
@override_settings(DEBUG=True)
@patch("baserow_premium.row_comments.signals.row_comment_created.send")
def test_row_comment_created_signal_called(mock_row_comment_created, data_fixture):
user = data_fixture.create_user(first_name="test_user")
table, fields, rows = data_fixture.build_table(
def test_row_comment_created_signal_called(
mock_row_comment_created, premium_data_fixture
):
user = premium_data_fixture.create_user(
first_name="test_user", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)

View file

@ -2,15 +2,19 @@ from unittest.mock import patch
import pytest
from freezegun import freeze_time
from django.test.utils import override_settings
from baserow_premium.row_comments.handler import RowCommentHandler
@pytest.mark.django_db(transaction=True)
@override_settings(DEBUG=True)
@patch("baserow.ws.registries.broadcast_to_channel_group")
def test_row_comment_created(mock_broadcast_to_channel_group, data_fixture):
user = data_fixture.create_user(first_name="test_user")
table, fields, rows = data_fixture.build_table(
def test_row_comment_created(mock_broadcast_to_channel_group, premium_data_fixture):
user = premium_data_fixture.create_user(
first_name="test_user", has_active_premium_license=True
)
table, fields, rows = premium_data_fixture.build_table(
columns=[("text", "text")], rows=["first row"], user=user
)

View file

@ -1,6 +1,19 @@
import { AdminType } from '@baserow/modules/core/adminTypes'
import { PremiumPlugin } from '@baserow_premium/plugins'
export class DashboardType extends AdminType {
class PremiumAdminType extends AdminType {
getDeactivatedText() {
return this.app.i18n.t('premium.deactivated')
}
isDeactivated() {
return !PremiumPlugin.hasValidPremiumLicense(
this.app.store.getters['auth/getAdditionalUserData']
)
}
}
export class DashboardType extends PremiumAdminType {
static getType() {
return 'dashboard'
}
@ -22,7 +35,7 @@ export class DashboardType extends AdminType {
}
}
export class UsersAdminType extends AdminType {
export class UsersAdminType extends PremiumAdminType {
static getType() {
return 'users'
}
@ -44,7 +57,7 @@ export class UsersAdminType extends AdminType {
}
}
export class GroupsAdminType extends AdminType {
export class GroupsAdminType extends PremiumAdminType {
static getType() {
return 'groups'
}
@ -65,3 +78,25 @@ export class GroupsAdminType extends AdminType {
return 3
}
}
export class LicensesAdminType extends AdminType {
static getType() {
return 'licenses'
}
getIconClass() {
return 'certificate'
}
getName() {
return 'Licenses'
}
getRouteName() {
return 'admin-licenses'
}
getOrder() {
return 10000
}
}

Some files were not shown because too many files have changed in this diff Show more