diff --git a/contracts/admin.py b/contracts/admin.py
index 1cbf1f2346eafa479828a30288ecf285023b2cde..ef8ee1950457a28a91705d435e0d6c3758811dad 100644
--- a/contracts/admin.py
+++ b/contracts/admin.py
@@ -173,6 +173,17 @@ class ContractAdmin(MarkdownxGuardedModelAdmin, FieldsetsInlineMixin, NestedMode
             for index, fieldset in enumerate(fieldsets_with_inlines)
         ]
 
+    def get_queryset(self, request):
+        queryset = super().get_queryset(request)
+
+        if not request.user.has_perm("contracts.view_confidential"):
+            queryset = queryset.filter(is_public=True)
+
+        if not request.user.has_perm("contracts.approve"):
+            queryset = queryset.filter(is_approved=True)
+
+        return queryset
+
     def save_model(self, request, obj, form, change) -> None:
         if obj.created_by is None:
             obj.created_by = request.user
@@ -262,25 +273,50 @@ class SigneeAdmin(MarkdownxGuardedModelAdmin):
 
     form = SigneeAdminForm
 
-    fields = (
-        "name",
-        "entity_type",
-        "address_street_with_number",
-        "address_district",
-        "address_zip",
-        "address_country",
-        "ico_number",
-        "load_ares_data_button",
-        "date_of_birth",
-        "department",
-        "role",
-    )
-
     readonly_fields = ("load_ares_data_button",)
 
     list_filter = ("entity_type",)
     list_display = ("name", "entity_type")
 
+    def get_fields(self, request, obj=None):
+        fields = [
+            "name",
+            "entity_type",
+            "ico_number",
+            "department",
+            "role",
+        ]
+
+        if (
+            obj is None  # Creating
+            or obj.entity_has_public_address
+            or request.user.has_perm("contracts.view_confidential", obj)
+        ):
+            entity_type_index = fields.index("entity_type")
+
+            fields[entity_type_index:entity_type_index] = [
+                "address_street_with_number",
+                "address_district",
+                "address_zip",
+                "address_country",
+            ]
+
+            fields.insert(
+                fields.index("department") - 1,
+                "date_of_birth",
+            )
+
+        if (
+            obj is None  # Allowed to create
+            or request.user.has_perm("contracts.edit_signee", obj)
+        ):
+            fields.insert(
+                fields.index("ico_number"),
+                "load_ares_data_button"
+            )
+
+        return fields
+
     def load_ares_data_button(self, obj):
         return format_html(
             '<button type="button" id="load_ares_data">Načíst data</button>'
diff --git a/contracts/migrations/0013_alter_contractee_address_country_and_more.py b/contracts/migrations/0013_alter_contractee_address_country_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..e2f5af7719a7b34749d68ee6066b62eb0d3b19ed
--- /dev/null
+++ b/contracts/migrations/0013_alter_contractee_address_country_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.1.4 on 2023-03-24 10:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contracts', '0012_alter_contractee_address_country_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='contractee',
+            name='address_country',
+            field=models.CharField(default='CZ', max_length=256, verbose_name='Země'),
+        ),
+        migrations.AlterField(
+            model_name='signee',
+            name='address_country',
+            field=models.CharField(default='CZ', max_length=256, verbose_name='Země'),
+        ),
+    ]
diff --git a/oidc/auth.py b/oidc/auth.py
index 6d7be2d8ecace57005ed76618db44910ab105d90..975d82757265c2d41f53ce7f6c6a345bfacc9252 100644
--- a/oidc/auth.py
+++ b/oidc/auth.py
@@ -1,19 +1,28 @@
+import typing
 import logging
 
 import jwt
-from django.conf import settings
 from django.contrib.auth.models import Group
+from django.conf import settings
 from pirates.auth import PiratesOIDCAuthenticationBackend
 
 logging.basicConfig(level=logging.DEBUG)
 
 
 class RegistryOIDCAuthenticationBackend(PiratesOIDCAuthenticationBackend):
-    def _assign_new_user_groups(self, user, access_token, user_groups=None) -> None:
+    def _assign_new_user_groups(
+        self,
+        user,
+        access_token: dict,
+        user_groups: typing.Union[None, list] = None
+    ) -> None:
         if user_groups is None:
             user_groups = user.groups.all()
 
         for group in access_token["groups"]:
+            if group.startswith("_"):
+                continue
+
             group_name = f"sso_{group}"
 
             group = Group.objects.filter(name=group_name)
@@ -27,9 +36,12 @@ class RegistryOIDCAuthenticationBackend(PiratesOIDCAuthenticationBackend):
             if group not in user_groups:
                 user.groups.add(group)
 
-        user.save()
-
-    def _remove_old_user_groups(self, user, access_token, user_groups=None) -> None:
+    def _remove_old_user_groups(
+        self,
+        user,
+        access_token: dict,
+        user_groups: typing.Union[None, list] = None
+    ) -> None:
         if user_groups is None:
             user_groups = user.groups.all()
 
@@ -50,10 +62,17 @@ class RegistryOIDCAuthenticationBackend(PiratesOIDCAuthenticationBackend):
         user_groups = user.groups.all()
 
         self._remove_old_user_groups(
-            user, decoded_access_token, user_groups=user_groups
+            user,
+            decoded_access_token,
+            user_groups=user_groups
         )
         self._assign_new_user_groups(
-            user, decoded_access_token, user_groups=user_groups
+            user,
+            decoded_access_token,
+            user_groups=user_groups
         )
 
+        user.update_group_based_admin()
+        user.save(saved_by_auth=True)
+
         return user
diff --git a/run.sh b/run.sh
index 416f8b5af8e41c7f70372fa9aae0ee92b8bf332b..f9c9d4f10b89680873d203d95df033ca1f59f179 100644
--- a/run.sh
+++ b/run.sh
@@ -3,8 +3,9 @@
 # exit on error
 set -e
 
-# migrate database
+# Migrate database
+python manage.py makemigrations  # Custom Group model
 python manage.py migrate
 
-# start webserver
+# Start webserver
 exec gunicorn -c gunicorn.conf.py registry.wsgi
diff --git a/users/migrations/0002_user_is_staff_based_on_group.py b/users/migrations/0002_user_is_staff_based_on_group.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f3b12c3a6a264e05934c2ff99d18fb5c6fece10
--- /dev/null
+++ b/users/migrations/0002_user_is_staff_based_on_group.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-03-24 10:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='is_staff_based_on_group',
+            field=models.BooleanField(default=True, verbose_name='Admin přístup dle členství ve skupině'),
+        ),
+    ]
diff --git a/users/migrations/0003_alter_user_is_staff_based_on_group.py b/users/migrations/0003_alter_user_is_staff_based_on_group.py
new file mode 100644
index 0000000000000000000000000000000000000000..e08a2ff2bcb09d5c658a728a7894c8f868f3b3b3
--- /dev/null
+++ b/users/migrations/0003_alter_user_is_staff_based_on_group.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-03-24 10:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0002_user_is_staff_based_on_group'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='user',
+            name='is_staff_based_on_group',
+            field=models.BooleanField(default=True, help_text='Určuje, zda bude "Administrační přístup" uživatele definován dle členství ve skupinách, nebo podle speciálního nastavení zde.', verbose_name='Administrační přístup dle členství ve skupině'),
+        ),
+    ]
diff --git a/users/models.py b/users/models.py
index fa51eb64eaa82299528a58a085c212c5eb49614a..6683ec6c2ce806f900bda8f1cbf2c1b523711db0 100644
--- a/users/models.py
+++ b/users/models.py
@@ -1,7 +1,19 @@
+from django.db import models
+from django.contrib.auth.models import Group
 from pirates import models as pirates_models
 
 
 class User(pirates_models.AbstractUser):
+    is_staff_based_on_group = models.BooleanField(
+        default=True,
+        verbose_name="Administrační přístup dle členství ve skupině",
+        help_text=(
+            "Určuje, zda bude \"Administrační přístup\" uživatele "
+            "definován dle členství ve skupinách, nebo podle "
+            "speciálního nastavení zde."
+        )
+    )
+
     def set_unusable_password(self) -> None:
         # Purely for compatibility with Guardian
         pass
@@ -14,19 +26,65 @@ class User(pirates_models.AbstractUser):
 
         return f"{first_name}{self.last_name}"
 
+    # https://docs.djangoproject.com/en/4.1/ref/models/instances/#customizing-model-loading
+    @classmethod
+    def from_db(cls, db, field_names, values):
+        # Default implementation of from_db() (subject to change and could
+        # be replaced with super()).
+        if len(values) != len(cls._meta.concrete_fields):
+            values = list(values)
+            values.reverse()
+            values = [
+                values.pop() if f.attname in field_names else models.DEFERRED
+                for f in cls._meta.concrete_fields
+            ]
+
+        instance = cls(*values)
+        instance._state.adding = False
+        instance._state.db = db
+
+        # customization to store the original field values on the instance
+        instance._loaded_values = dict(
+            zip(
+                field_names,
+                (
+                    value
+                    for value in values
+                    if value is not models.DEFERRED
+                )
+            )
+        )
+
+        return instance
+
+    def save(self, *args, saved_by_auth: bool = False, **kwargs):
+        if (
+            not self._state.adding
+            and not saved_by_auth
+            and self._loaded_values["is_staff"] != self.is_staff
+        ):
+            self.is_staff_based_on_group = False
+
+        return super().save(*args, **kwargs)
+
+    def update_group_based_admin(self) -> None:
+        if not self.is_staff_based_on_group:
+            return
+
+        self.is_staff_based_on_group = True
+        self.is_staff = self.groups.filter(is_staff=True).exists()
+
     @property
     def can_approve_contracts(self) -> bool:
-        # TODO: Do we need the superuser check?
-        return self.is_superuser or self.has_perm("contracts.approve")
+        return self.has_perm("contracts.approve")
 
     @property
     def can_create_contracts(self) -> bool:
-        # TODO: Do we need the superuser check?
-        return self.is_superuser or self.has_perm("contracts.add")
+        return self.has_perm("contracts.add")
 
     @property
     def can_view_confidential(self) -> bool:
-        return self.is_superuser or self.has_perm("contracts.view_confidential")
+        return self.has_perm("contracts.view_confidential")
 
     @property
     def contracts_to_approve_count(self) -> int:
@@ -41,3 +99,12 @@ class User(pirates_models.AbstractUser):
         app_label = "users"
         verbose_name = "Uživatel"
         verbose_name_plural = "Uživatelé"
+
+
+if not hasattr(Group, "is_staff"):
+    is_staff = models.BooleanField(
+        default=False,
+        verbose_name="Administrační přístup",
+        help_text="Určuje, zda se skupina může přihlásit do správy tohoto webu.",
+    )
+    is_staff.contribute_to_class(Group, "is_staff")