diff --git a/authentik/tenants/tests/test_api.py b/authentik/tenants/tests/test_api.py index f7e9b295a..24cf39e3b 100644 --- a/authentik/tenants/tests/test_api.py +++ b/authentik/tenants/tests/test_api.py @@ -1,59 +1,19 @@ """Test Tenant API""" from json import loads -from django.core.management import call_command -from django.db import connection from django.urls import reverse -from rest_framework.test import APILiveServerTestCase, APITestCase, APITransactionTestCase from authentik.lib.config import CONFIG from authentik.lib.generators import generate_id +from authentik.tenants.tests.utils import TenantAPITestCase TENANTS_API_KEY = generate_id() HEADERS = {"Authorization": f"Bearer {TENANTS_API_KEY}"} -class TestAPI(APITransactionTestCase): +class TestAPI(TenantAPITestCase): """Test api view""" - def _fixture_teardown(self): - for db_name in self._databases_names(include_mirrors=False): - call_command( - "flush", - verbosity=0, - interactive=False, - database=db_name, - reset_sequences=False, - allow_cascade=True, - inhibit_post_migrate=False, - ) - - def setUp(self): - call_command("migrate_schemas", schema="template", tenant=True) - - def assertSchemaExists(self, schema_name): - with connection.cursor() as cursor: - cursor.execute( - f"SELECT * FROM information_schema.schemata WHERE schema_name = '{schema_name}';" - ) - self.assertEqual(cursor.rowcount, 1) - - cursor.execute( - "SELECT * FROM information_schema.tables WHERE table_schema = 'template';" - ) - expected_tables = cursor.rowcount - cursor.execute( - f"SELECT * FROM information_schema.tables WHERE table_schema = '{schema_name}';" - ) - self.assertEqual(cursor.rowcount, expected_tables) - - def assertSchemaDoesntExist(self, schema_name): - with connection.cursor() as cursor: - cursor.execute( - f"SELECT * FROM information_schema.schemata WHERE schema_name = '{schema_name}';" - ) - self.assertEqual(cursor.rowcount, 0) - @CONFIG.patch("outposts.disable_embedded_outpost", True) @CONFIG.patch("tenants.enabled", True) @CONFIG.patch("tenants.api_key", TENANTS_API_KEY) diff --git a/authentik/tenants/tests/test_domain.py b/authentik/tenants/tests/test_domain.py new file mode 100644 index 000000000..b234c762e --- /dev/null +++ b/authentik/tenants/tests/test_domain.py @@ -0,0 +1,51 @@ +"""Test Domain API""" +from json import loads + +from django.urls import reverse + +from authentik.lib.config import CONFIG +from authentik.lib.generators import generate_id +from authentik.tenants.models import Domain, Tenant +from authentik.tenants.tests.utils import TenantAPITestCase + +TENANTS_API_KEY = generate_id() +HEADERS = {"Authorization": f"Bearer {TENANTS_API_KEY}"} + + +class TestDomainAPI(TenantAPITestCase): + def setUp(self): + super().setUp() + self.tenant = Tenant.objects.create( + name=generate_id(), schema_name="t_" + generate_id().lower() + ) + + def tearDown(self): + self.tenant.delete() + + @CONFIG.patch("outposts.disable_embedded_outpost", True) + @CONFIG.patch("tenants.enabled", True) + @CONFIG.patch("tenants.api_key", TENANTS_API_KEY) + def test_domain(self): + """Test domain creation""" + response = self.client.post( + reverse("authentik_api:domain-list"), + headers=HEADERS, + data={"tenant": self.tenant.pk, "domain": "test.domain"}, + ) + self.assertEqual(response.status_code, 201) + body = loads(response.content.decode()) + self.assertEqual(self.tenant.domains.get(domain="test.domain").pk, body["id"]) + self.assertEqual(self.tenant.domains.get(domain="test.domain").is_primary, True) + + response = self.client.post( + reverse("authentik_api:domain-list"), + headers=HEADERS, + data={"tenant": self.tenant.pk, "domain": "newprimary.domain", "is_primary": True}, + ) + self.assertEqual(response.status_code, 201) + self.assertEqual( + Domain.objects.get(tenant=self.tenant, domain="newprimary.domain").is_primary, True + ) + self.assertEqual( + Domain.objects.get(tenant=self.tenant, domain="test.domain").is_primary, False + ) diff --git a/authentik/tenants/tests/test_settings.py b/authentik/tenants/tests/test_settings.py new file mode 100644 index 000000000..2df1b0da4 --- /dev/null +++ b/authentik/tenants/tests/test_settings.py @@ -0,0 +1,50 @@ +"""Test Settings API""" +from json import loads + +from django.urls import reverse +from django_tenants.utils import get_public_schema_name + +from authentik.lib.config import CONFIG +from authentik.lib.generators import generate_id +from authentik.tenants.models import Domain, Tenant +from authentik.tenants.tests.utils import TenantAPITestCase + +TENANTS_API_KEY = generate_id() +HEADERS = {"Authorization": f"Bearer {TENANTS_API_KEY}"} + + +# class TestSettingsAPI(TenantAPITestCase): +# def setUp(self): +# super().setUp() +# self.tenant_1 = Tenant.objects.get(schema_name=get_public_schema_name()) +# self.tenant_2 = Tenant.objects.create( +# name=generate_id(), schema_name="t_" + generate_id().lower() +# ) +# +# @CONFIG.patch("outposts.disable_embedded_outpost", True) +# @CONFIG.patch("tenants.enabled", True) +# @CONFIG.patch("tenants.api_key", TENANTS_API_KEY) +# def test_domain(self): +# """Test domain creation""" +# response = self.client.post( +# reverse("authentik_api:domain-list"), +# headers=HEADERS, +# data={"tenant": self.tenant.pk, "domain": "test.domain"}, +# ) +# self.assertEqual(response.status_code, 201) +# body = loads(response.content.decode()) +# self.assertEqual(self.tenant.domains.get(domain="test.domain").pk, body["id"]) +# self.assertEqual(self.tenant.domains.get(domain="test.domain").is_primary, True) +# +# response = self.client.post( +# reverse("authentik_api:domain-list"), +# headers=HEADERS, +# data={"tenant": self.tenant.pk, "domain": "newprimary.domain", "is_primary": True}, +# ) +# self.assertEqual(response.status_code, 201) +# self.assertEqual( +# Domain.objects.get(tenant=self.tenant, domain="newprimary.domain").is_primary, True +# ) +# self.assertEqual( +# Domain.objects.get(tenant=self.tenant, domain="test.domain").is_primary, False +# ) diff --git a/authentik/tenants/tests/utils.py b/authentik/tenants/tests/utils.py new file mode 100644 index 000000000..110b51ec9 --- /dev/null +++ b/authentik/tenants/tests/utils.py @@ -0,0 +1,56 @@ +from django.core.management import call_command +from django.db import connection, connections +from rest_framework.test import APITransactionTestCase + + +class TenantAPITestCase(APITransactionTestCase): + # Overriden to force TRUNCATE CASCADE + def _fixture_teardown(self): + for db_name in self._databases_names(include_mirrors=False): + call_command( + "flush", + verbosity=0, + interactive=False, + database=db_name, + reset_sequences=False, + allow_cascade=True, + inhibit_post_migrate=False, + ) + + with connections[db_name].cursor() as cursor: + cursor.execute( + "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname !~ 'pg_*' AND nspname != 'information_schema' AND nspname != 'public' AND nspname != 'template'" + ) + schemas = cursor.fetchall() + for row in schemas: + schema = row[0] + cursor.execute(f"DROP SCHEMA {schema}") + + def setUp(self): + call_command("migrate_schemas", schema="template", tenant=True) + + def assertSchemaExists(self, schema_name): + with connection.cursor() as cursor: + cursor.execute( + f"SELECT * FROM information_schema.schemata WHERE schema_name = %(schema_name)s", + {"schema_name": schema_name}, + ) + self.assertEqual(cursor.rowcount, 1) + + cursor.execute( + "SELECT * FROM information_schema.tables WHERE table_schema = 'template'" + ) + expected_tables = cursor.rowcount + cursor.execute( + f"SELECT * FROM information_schema.tables WHERE table_schema = %(schema_name)s", + {"schema_name": schema_name}, + ) + self.assertEqual(cursor.rowcount, expected_tables) + + def assertSchemaDoesntExist(self, schema_name): + with connection.cursor() as cursor: + cursor.execute( + f"SELECT * FROM information_schema.schemata WHERE schema_name = %(schema_name)s", + {"schema_name": schema_name}, + ) + self.assertEqual(cursor.rowcount, 0) diff --git a/authentik/tenants/urls.py b/authentik/tenants/urls.py index 6d6768d2d..827d9fcf1 100644 --- a/authentik/tenants/urls.py +++ b/authentik/tenants/urls.py @@ -1,12 +1,16 @@ """API URLs""" from django.urls import path -from authentik.tenants.api import SettingsView, TenantViewSet +from authentik.tenants.api import DomainViewSet, SettingsView, TenantViewSet api_urlpatterns = [ path("admin/settings/", SettingsView.as_view(), name="tenant_settings"), ( - "tenants", + "tenants/tenants", TenantViewSet, ), + ( + "tenants/domains", + DomainViewSet, + ), ] diff --git a/schema.yml b/schema.yml index cc728706b..dd717aba8 100644 --- a/schema.yml +++ b/schema.yml @@ -27839,9 +27839,221 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /tenants/: + /tenants/domains/: get: - operationId: tenants_list + operationId: tenants_domains_list + description: Domain ViewSet + parameters: + - name: ordering + required: false + in: query + description: Which field to use when ordering the results. + schema: + type: string + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + - name: page_size + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: search + required: false + in: query + description: A search term. + schema: + type: string + tags: + - tenants + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedDomainList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: tenants_domains_create + description: Domain ViewSet + tags: + - tenants + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DomainRequest' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Domain' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /tenants/domains/{id}/: + get: + operationId: tenants_domains_retrieve + description: Domain ViewSet + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Domain. + required: true + tags: + - tenants + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Domain' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: tenants_domains_update + description: Domain ViewSet + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Domain. + required: true + tags: + - tenants + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DomainRequest' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Domain' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: tenants_domains_partial_update + description: Domain ViewSet + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Domain. + required: true + tags: + - tenants + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDomainRequest' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Domain' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: tenants_domains_destroy + description: Domain ViewSet + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Domain. + required: true + tags: + - tenants + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /tenants/tenants/: + get: + operationId: tenants_tenants_list description: Tenant Viewset parameters: - name: ordering @@ -27870,8 +28082,6 @@ paths: type: string tags: - tenants - security: - - authentik: [] responses: '200': content: @@ -27892,7 +28102,7 @@ paths: $ref: '#/components/schemas/GenericError' description: '' post: - operationId: tenants_create + operationId: tenants_tenants_create description: Tenant Viewset tags: - tenants @@ -27902,8 +28112,6 @@ paths: schema: $ref: '#/components/schemas/TenantRequest' required: true - security: - - authentik: [] responses: '201': content: @@ -27923,9 +28131,9 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /tenants/{tenant_uuid}/: + /tenants/tenants/{tenant_uuid}/: get: - operationId: tenants_retrieve + operationId: tenants_tenants_retrieve description: Tenant Viewset parameters: - in: path @@ -27937,8 +28145,6 @@ paths: required: true tags: - tenants - security: - - authentik: [] responses: '200': content: @@ -27959,7 +28165,7 @@ paths: $ref: '#/components/schemas/GenericError' description: '' put: - operationId: tenants_update + operationId: tenants_tenants_update description: Tenant Viewset parameters: - in: path @@ -27977,8 +28183,6 @@ paths: schema: $ref: '#/components/schemas/TenantRequest' required: true - security: - - authentik: [] responses: '200': content: @@ -27999,7 +28203,7 @@ paths: $ref: '#/components/schemas/GenericError' description: '' patch: - operationId: tenants_partial_update + operationId: tenants_tenants_partial_update description: Tenant Viewset parameters: - in: path @@ -28016,8 +28220,6 @@ paths: application/json: schema: $ref: '#/components/schemas/PatchedTenantRequest' - security: - - authentik: [] responses: '200': content: @@ -28038,7 +28240,7 @@ paths: $ref: '#/components/schemas/GenericError' description: '' delete: - operationId: tenants_destroy + operationId: tenants_tenants_destroy description: Tenant Viewset parameters: - in: path @@ -28050,8 +28252,6 @@ paths: required: true tags: - tenants - security: - - authentik: [] responses: '204': description: No response body @@ -34935,6 +35135,18 @@ components: required: - pagination - results + PaginatedDomainList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/Domain' + required: + - pagination + - results PaginatedDummyPolicyList: type: object properties: @@ -36619,6 +36831,19 @@ components: nullable: true description: Certificate/Key used for authentication. Can be left empty for no authentication. + PatchedDomainRequest: + type: object + description: Domain Serializer + properties: + domain: + type: string + minLength: 1 + maxLength: 253 + is_primary: + type: boolean + tenant: + type: string + format: uuid PatchedDummyPolicyRequest: type: object description: Dummy Policy Serializer