From 8f189dd4cb47b6f041735819639e1559bba0b585 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 10:36:40 +0000 Subject: [PATCH 01/36] feat(permission): Add new models --- apps/core/models/__init__.py | 1 + apps/core/models/board_permission.py | 98 ++++++++++++++++++++++++++++ apps/user/models/__init__.py | 3 + apps/user/models/group.py | 40 ++++++++++++ apps/user/models/user_profile.py | 13 ++-- apps/user/models/usergroup.py | 35 ++++++++++ 6 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 apps/core/models/board_permission.py create mode 100644 apps/user/models/group.py create mode 100644 apps/user/models/usergroup.py diff --git a/apps/core/models/__init__.py b/apps/core/models/__init__.py index 52eaaa10..fdc2cc6f 100644 --- a/apps/core/models/__init__.py +++ b/apps/core/models/__init__.py @@ -7,6 +7,7 @@ from .block import * from .board import * from .board_group import * +from .board_permission import * from .comment import * from .comment_log import * from .communication_article import * diff --git a/apps/core/models/board_permission.py b/apps/core/models/board_permission.py new file mode 100644 index 00000000..9a3c2635 --- /dev/null +++ b/apps/core/models/board_permission.py @@ -0,0 +1,98 @@ +from enum import IntEnum +from typing import Any + +from django.conf import settings +from django.db import models + +from apps.user.models import Group, UserProfile + +from .board import Board + + +class BoardAccessPermissionType(IntEnum): + DENY = 0 + READ = 1 + WRITE = 2 + COMMENT = 3 + DELETE = 4 + + +class BoardAccessPermission: + def __init__( + self, user: UserProfile, board: Board + ) -> None: # permission: [BoardAccessPermissionType] + self.user = user + self.board = board + self.READ = False + self.WRITE = False + self.COMMENT = False + self.DELETE = False + self.DENY = False + + def setPermission(self, permission: BoardAccessPermissionType) -> None: + if self.DENY: + return + + if permission == BoardAccessPermissionType.DENY: + self.DENY = True + self.READ = False + self.WRITE = False + self.COMMENT = False + self.DELETE = False + elif permission == BoardAccessPermissionType.READ: + self.READ = True + elif permission == BoardAccessPermissionType.WRITE: + self.WRITE = True + elif permission == BoardAccessPermissionType.COMMENT: + self.COMMENT = True + elif permission == BoardAccessPermissionType.DELETE: + self.DELETE = True + + +class BoardPermission(models.Model): + class Meta: + verbose_name = "BoardPermission" + verbose_name_plural = "BoardPermissions" + unique_together = (("groupid", "boardid", "permission"),) + + group_id = models.ForeignKey( + on_delete=models.CASCADE, + to="core.Groups", + db_index=True, + related_name="group_id", + verbose_name="group", + ) + board_slug = models.ForeignKey( + on_delete=models.CASCADE, + to="core.Board", + db_index=True, + related_name="slug", + verbose_name="board", + ) + permission: int = models.SmallIntegerField( + verbose_name="permission", + null=False, + ) + + @staticmethod + def permission_list_by_group(self, group: Group, board: int) -> bool: + return BoardPermission.objects.filter(groupid=group, boardid=board) + + # @staticmethod + # def permission_list_by_user(self, user: UserProfile, board: int) -> bool: + # groups = user.groups + # permissions = {} # board_slug: [permission] + # for group in groups: + # groupPerms = BoardPermission.objects.filter(groupid=group, boardid=board) + + def permission_list_by_user_board( + self, user: UserProfile, board: Board + ) -> BoardAccessPermission: + groups = user.groups + permissions = BoardAccessPermission(user, board) + for group in groups: + groupPerms = BoardPermission.objects.filter(groupid=group, boardid=board) + for perm in groupPerms: + permissions.setPermission(perm.permission) + + return permissions diff --git a/apps/user/models/__init__.py b/apps/user/models/__init__.py index a19f341f..de393058 100644 --- a/apps/user/models/__init__.py +++ b/apps/user/models/__init__.py @@ -1,3 +1,6 @@ from .fcm_token import * +from .group import * from .signals import * +from .user import * from .user_profile import * +from .usergroup import * diff --git a/apps/user/models/group.py b/apps/user/models/group.py new file mode 100644 index 00000000..bb51cada --- /dev/null +++ b/apps/user/models/group.py @@ -0,0 +1,40 @@ +from django.conf import settings +from django.db import models + + +class Group(models.Model): + class Meta: + verbose_name = "Group" + verbose_name_plural = "Groups" + + group_id = models.AutoField( + verbose_name="Group ID", + primary_key=True, + db_index=True, + null=False, + ) + name = models.CharField( + verbose_name="Group name", + max_length=32, + null=False, + ) + description = models.CharField( + verbose_name="Group description", + max_length=128, + null=True, + ) + is_official = models.BooleanField( + verbose_name="공식 단체 또는 학생단체", + default=False, + ) + + def __str__(self) -> str: + return self.name + + @staticmethod + def search_by_name(name: str) -> list: + return Group.objects.filter(name=name) + + @staticmethod + def search_by_id(group_id: int) -> "Group": + return Group.objects.get(group_id=group_id) diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index ac1c1f19..23eb1f7c 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -7,6 +7,7 @@ from django.utils.functional import cached_property from django.utils.translation import gettext_lazy +from apps.user.models import UserGroup from ara.db.models import MetaDataModel from ara.settings import MIN_TIME @@ -21,7 +22,7 @@ class Meta(MetaDataModel.Meta): ("nickname", "is_newara", "deleted_at"), ) - class UserGroup(models.IntegerChoices): + class OldUserGroup(models.IntegerChoices): # 뉴아라 계정을 만들지 않은 사람들 UNAUTHORIZED = 0, gettext_lazy("Unauthorized user") # 카이스트 메일을 가진 사람 (학생, 교직원) @@ -39,7 +40,7 @@ class UserGroup(models.IntegerChoices): # 뉴스게시판 관리인 NEWS_BOARD_ADMIN = 7, gettext_lazy("News board admin") - OFFICIAL_GROUPS = [UserGroup.STORE_EMPLOYEE, UserGroup.KAIST_ORG] + OLD_OFFICIAL_GROUPS = [OldUserGroup.STORE_EMPLOYEE, OldUserGroup.KAIST_ORG] uid = models.CharField( null=True, @@ -85,10 +86,6 @@ class UserGroup(models.IntegerChoices): default=False, verbose_name="정치/사회성 보기", ) - group = models.IntegerField( - choices=UserGroup.choices, - default=UserGroup.UNAUTHORIZED, - ) user = models.OneToOneField( on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL, @@ -148,3 +145,7 @@ def is_official(self) -> bool: @cached_property def is_school_admin(self) -> bool: return self.group == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN + + @cached_property + def groups(self) -> list: + return UserGroup.search_by_user(self.user) diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py new file mode 100644 index 00000000..6b25df7a --- /dev/null +++ b/apps/user/models/usergroup.py @@ -0,0 +1,35 @@ +from django.conf import settings +from django.db import models + + +class UserGroup(models.Model): + class Meta: + verbose_name = "사용자 그룹" + verbose_name_plural = "사용자가 속한 그룹 목록" + unique_together = (("userid", "groupid"),) + + user_id = models.ForeignKey( + verbose_name="사용자", + on_delete=models.CASCADE, + to=settings.AUTH_USER_MODEL, + primary_key=True, + db_index=True, + related_name="user_id", + verbose_name="user", + ) + + group_id = models.ForeignKey( + verbose_name="그룹", + on_delete=models.CASCADE, + to="user.Group", + db_index=True, + related_name="group_id", + ) + + @staticmethod + def search_by_user(self, user: int) -> list: + return UserGroup.objects.filter(user_id=user) + + @staticmethod + def search_by_group(self, group: int) -> list: # WARNING: Too many results + return UserGroup.objects.filter(group_id=group) From 9344b5473ea98ff6162df9fe75ad9696543b6c10 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 10:40:35 +0000 Subject: [PATCH 02/36] refactor(calendar): move meta to top --- apps/core/models/board_group.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/core/models/board_group.py b/apps/core/models/board_group.py index 3f78fc04..b2d6804d 100644 --- a/apps/core/models/board_group.py +++ b/apps/core/models/board_group.py @@ -2,6 +2,10 @@ class BoardGroup(models.Model): + class Meta: + verbose_name = "게시판 그룹" + verbose_name_plural = "게시판 그룹 목록" + ko_name = models.CharField( verbose_name="게시판 그룹 국문 이름", max_length=64, @@ -16,9 +20,5 @@ class BoardGroup(models.Model): unique=True, ) - class Meta: - verbose_name = "게시판 그룹" - verbose_name_plural = "게시판 그룹 목록" - def __str__(self) -> str: return self.ko_name From 9ed4cb9046ee614782cbe8aa4ab887af5ca9eea6 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 12:27:34 +0000 Subject: [PATCH 03/36] feat(permission): Fix all permission function request to new one. --- apps/core/models/article.py | 9 ++++----- apps/core/models/board.py | 21 +++++++++++++++------ apps/core/models/board_permission.py | 27 +++++++++++++-------------- apps/core/permissions/article.py | 4 +--- apps/core/serializers/article.py | 6 +++--- apps/core/serializers/board.py | 14 ++++---------- apps/core/views/viewsets/comment.py | 5 +---- tests/test_articles.py | 8 ++------ 8 files changed, 43 insertions(+), 51 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 4525bc4e..fc1d00f8 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -9,6 +9,7 @@ from django.utils.functional import cached_property from django.utils.translation import gettext +from apps.core.models import Board from apps.user.views.viewsets import get_profile_picture, hashlib from ara.classes.decorator import cache_by_user from ara.db.models import MetaDataModel @@ -21,7 +22,7 @@ ) from .block import Block -from .board import BoardAccessPermissionType, NameType +from .board import NameType from .comment import Comment from .communication_article import SchoolResponseStatus from .report import Report @@ -111,7 +112,7 @@ class Article(MetaDataModel): db_index=True, default=None, ) - parent_board = models.ForeignKey( + parent_board: Board = models.ForeignKey( verbose_name="게시판", to="core.Board", on_delete=models.CASCADE, @@ -315,9 +316,7 @@ def hidden_reasons(self, user: User) -> list: if self.is_content_social and not user.profile.see_social: reasons.append(ArticleHiddenReason.SOCIAL_CONTENT) # 혹시 몰라 여기 두기는 하는데 여기 오기전에 Permission에서 막혀야 함 - if not self.parent_board.group_has_access_permission( - BoardAccessPermissionType.READ, user.profile.group - ): + if not self.parent_board.permission_list_by_user(user).READ: reasons.append(ArticleHiddenReason.ACCESS_DENIED_CONTENT) return reasons diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 45658b02..9352009d 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -3,8 +3,11 @@ from django.db import models from django_extensions.db.fields import AutoSlugField +from apps.user.models import Group, UserProfile from ara.db.models import MetaDataModel + from .board_group import BoardGroup +from .board_permission import BoardAccessPermission, BoardPermission class NameType(IntFlag): @@ -13,7 +16,7 @@ class NameType(IntFlag): REALNAME = auto() -class BoardAccessPermissionType(IntEnum): +class OldBoardAccessPermissionType(IntEnum): READ = 0 WRITE = 1 COMMENT = 2 @@ -113,15 +116,21 @@ class Meta(MetaDataModel.Meta): def __str__(self) -> str: return self.ko_name - def group_has_access_permission( - self, access_type: BoardAccessPermissionType, group: int + def permission_list_by_group(self, group: Group) -> BoardAccessPermission: + return BoardPermission.permission_list_by_group(self, group) + + def permission_list_by_user(self, user: UserProfile) -> BoardAccessPermission: + return BoardPermission.permission_list_by_user(self, user) + + def old_group_has_access_permission( + self, access_type: OldBoardAccessPermissionType, group: int ) -> bool: mask = None - if access_type == BoardAccessPermissionType.READ: + if access_type == OldBoardAccessPermissionType.READ: mask = self.read_access_mask - elif access_type == BoardAccessPermissionType.WRITE: + elif access_type == OldBoardAccessPermissionType.WRITE: mask = self.write_access_mask - elif access_type == BoardAccessPermissionType.COMMENT: + elif access_type == OldBoardAccessPermissionType.COMMENT: mask = self.comment_access_mask else: # TODO: Handle error diff --git a/apps/core/models/board_permission.py b/apps/core/models/board_permission.py index 9a3c2635..b7beaf9d 100644 --- a/apps/core/models/board_permission.py +++ b/apps/core/models/board_permission.py @@ -19,9 +19,9 @@ class BoardAccessPermissionType(IntEnum): class BoardAccessPermission: def __init__( - self, user: UserProfile, board: Board + self, target: UserProfile | Group, board: Board ) -> None: # permission: [BoardAccessPermissionType] - self.user = user + self.target = target self.board = board self.READ = False self.WRITE = False @@ -75,18 +75,17 @@ class Meta: ) @staticmethod - def permission_list_by_group(self, group: Group, board: int) -> bool: - return BoardPermission.objects.filter(groupid=group, boardid=board) - - # @staticmethod - # def permission_list_by_user(self, user: UserProfile, board: int) -> bool: - # groups = user.groups - # permissions = {} # board_slug: [permission] - # for group in groups: - # groupPerms = BoardPermission.objects.filter(groupid=group, boardid=board) - - def permission_list_by_user_board( - self, user: UserProfile, board: Board + def permission_list_by_group(group: Group, board: Board) -> bool: + permissions = BoardAccessPermission(group, board) + groupPerms = BoardPermission.objects.filter(groupid=group, boardid=board) + for perm in groupPerms: + permissions.setPermission(perm.permission) + + return permissions + + @staticmethod + def permission_list_by_user( + user: UserProfile, board: Board ) -> BoardAccessPermission: groups = user.groups permissions = BoardAccessPermission(user, board) diff --git a/apps/core/permissions/article.py b/apps/core/permissions/article.py index 588ddc51..d1d9a2bd 100644 --- a/apps/core/permissions/article.py +++ b/apps/core/permissions/article.py @@ -15,6 +15,4 @@ class ArticleReadPermission(permissions.BasePermission): message = "해당 게시물에 대한 읽기 권한이 없습니다." def has_object_permission(self, request, view, obj: Article): - return obj.parent_board.group_has_access_permission( - BoardAccessPermissionType.READ, request.user.profile.group - ) + return obj.parent_board.permission_list_by_user(request.user).READ diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 919af38c..8e73fa2e 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -523,9 +523,9 @@ def validate_parent_board(self, board: Board): user_is_superuser = self.context["request"].user.is_superuser if not user_is_superuser and board.is_readonly: raise serializers.ValidationError(gettext("This board is read only.")) - user_has_write_permission = board.group_has_access_permission( - BoardAccessPermissionType.WRITE, self.context["request"].user.profile.group - ) + user_has_write_permission = board.permission_list_by_user( + self.context["request"].user + ).WRITE if not user_has_write_permission: raise exceptions.PermissionDenied() return board diff --git a/apps/core/serializers/board.py b/apps/core/serializers/board.py index 7415e5fd..84483c18 100644 --- a/apps/core/serializers/board.py +++ b/apps/core/serializers/board.py @@ -51,14 +51,8 @@ class Meta(BaseBoardSerializer.Meta): "user_writable", ] - def get_user_readable(self, obj): - user = self.context["request"].user - return obj.group_has_access_permission( - BoardAccessPermissionType.READ, user.profile.group - ) + def get_user_readable(self, obj: Board): + return obj.permission_list_by_user(self.context["request"].user).READ - def get_user_writable(self, obj): - user = self.context["request"].user - return obj.group_has_access_permission( - BoardAccessPermissionType.WRITE, user.profile.group - ) + def get_user_writable(self, obj: Board): + return obj.permission_list_by_user(self.context["request"].user).WRITE diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 75f4eedb..36b76100 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -64,10 +64,7 @@ def create(self, request, *args, **kwargs): # self.check_object_permissions(request, parent_article) # Check permission - user_group = request.user.profile.group - if parent_article.parent_board.group_has_access_permission( - BoardAccessPermissionType.COMMENT, user_group - ): + if parent_article.parent_board.permission_list_by_user(request.user).COMMENT: return super().create(request, *args, **kwargs) return response.Response( {"message": gettext("Permission denied")}, status=status.HTTP_403_FORBIDDEN diff --git a/tests/test_articles.py b/tests/test_articles.py index f522df3a..6353065e 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -339,9 +339,7 @@ def test_check_read_permission_when_get(self): for article in articles: res = self.http_request(user, "get", f"articles/{article.id}") - if article.parent_board.group_has_access_permission( - BoardAccessPermissionType.READ, user.profile.group - ): + if article.parent_board.permission_list_by_user(user).READ: assert res.status_code == status.HTTP_200_OK assert res.data["id"] == article.id else: @@ -378,9 +376,7 @@ def test_check_write_permission_when_create(self): }, ) - if board.group_has_access_permission( - BoardAccessPermissionType.WRITE, user.profile.group - ): + if board.permission_list_by_user(user).WRITE: assert res.status_code == status.HTTP_201_CREATED else: assert res.status_code == status.HTTP_403_FORBIDDEN From b5be2b0c8d8c36d1a4d34cdbdb72030551d830e6 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 14:00:59 +0000 Subject: [PATCH 04/36] feat(permission): remove related name from models --- apps/user/models/usergroup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py index 6b25df7a..c3a7c9ff 100644 --- a/apps/user/models/usergroup.py +++ b/apps/user/models/usergroup.py @@ -14,8 +14,6 @@ class Meta: to=settings.AUTH_USER_MODEL, primary_key=True, db_index=True, - related_name="user_id", - verbose_name="user", ) group_id = models.ForeignKey( @@ -23,7 +21,6 @@ class Meta: on_delete=models.CASCADE, to="user.Group", db_index=True, - related_name="group_id", ) @staticmethod From 0038fedf107c2a73884f4f8b6e233edd703ccbb2 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 14:01:28 +0000 Subject: [PATCH 05/36] feat(permission): move org_type to groups --- apps/user/models/user/manual.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/user/models/user/manual.py b/apps/user/models/user/manual.py index 6183d644..72512fc9 100644 --- a/apps/user/models/user/manual.py +++ b/apps/user/models/user/manual.py @@ -1,7 +1,7 @@ from django.conf import settings from django.db import models -from apps.user.models import UserProfile +from apps.user.models import Group, UserProfile from ara.db.models import MetaDataModel @@ -25,11 +25,6 @@ class Meta(MetaDataModel.Meta): verbose_name="업체/단체 이름", ) - org_type = models.IntegerField( - choices=UserProfile.UserGroup.choices, - default=UserProfile.UserGroup.UNAUTHORIZED, - ) - applicant_name = models.CharField( max_length=160, verbose_name="신청자 이름", From 62eca9f8f23fa47dc6ec88679693517e1a41daf0 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 14:02:00 +0000 Subject: [PATCH 06/36] feat(permission): add permission admin page --- apps/user/admin.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/apps/user/admin.py b/apps/user/admin.py index 185f7aaf..5e6a9ebb 100644 --- a/apps/user/admin.py +++ b/apps/user/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from apps.user.models import UserProfile +from apps.user.models import Group, UserGroup, UserProfile from apps.user.models.user.manual import ManualUser from ara.classes.admin import MetaDataModelAdmin @@ -10,7 +10,6 @@ class UserProfileAdmin(MetaDataModelAdmin): list_filter = ( "see_sexual", "see_social", - "group", "is_newara", ) list_display = ( @@ -18,7 +17,6 @@ class UserProfileAdmin(MetaDataModelAdmin): "sid", "nickname", "user", - "group", ) search_fields = ( "uid", @@ -37,3 +35,28 @@ class ManualUserAdmin(MetaDataModelAdmin): "applicant_name", "sso_email", ) + + +@admin.register(UserGroup) +class UserGroupAdmin(MetaDataModelAdmin): + list_display = ( + "user_id", + "group_id", + ) + search_fields = ( + "user_id", + "group_id", + ) + + +@admin.register(Group) +class GroupAdmin(MetaDataModelAdmin): + list_display = ( + "group_id", + "name", + "is_official", + ) + search_fields = ( + "id", + "name", + ) From 6dc93d70f6f2d692cd84c2035201a5a4d4a37ec5 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 14:02:32 +0000 Subject: [PATCH 07/36] refactor(permission): clear all imports and type hints --- apps/core/models/article.py | 3 +-- apps/core/models/board_permission.py | 16 +++++++++------- apps/core/permissions/article.py | 1 - apps/core/serializers/article.py | 2 +- apps/core/serializers/board.py | 2 +- apps/core/views/viewsets/comment.py | 2 +- apps/user/models/user_profile.py | 3 ++- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index fc1d00f8..eb6f4ed1 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -9,7 +9,6 @@ from django.utils.functional import cached_property from django.utils.translation import gettext -from apps.core.models import Board from apps.user.views.viewsets import get_profile_picture, hashlib from ara.classes.decorator import cache_by_user from ara.db.models import MetaDataModel @@ -22,7 +21,7 @@ ) from .block import Block -from .board import NameType +from .board import Board, NameType from .comment import Comment from .communication_article import SchoolResponseStatus from .report import Report diff --git a/apps/core/models/board_permission.py b/apps/core/models/board_permission.py index b7beaf9d..a4b4b237 100644 --- a/apps/core/models/board_permission.py +++ b/apps/core/models/board_permission.py @@ -1,12 +1,16 @@ +from __future__ import annotations + from enum import IntEnum -from typing import Any +from typing import TYPE_CHECKING, Any from django.conf import settings from django.db import models +import apps.core.models.board as board from apps.user.models import Group, UserProfile -from .board import Board +if TYPE_CHECKING: + from .board import Board class BoardAccessPermissionType(IntEnum): @@ -53,21 +57,19 @@ class BoardPermission(models.Model): class Meta: verbose_name = "BoardPermission" verbose_name_plural = "BoardPermissions" - unique_together = (("groupid", "boardid", "permission"),) + unique_together = (("group_id", "board_slug", "permission"),) group_id = models.ForeignKey( on_delete=models.CASCADE, - to="core.Groups", + to="user.Group", db_index=True, - related_name="group_id", verbose_name="group", ) board_slug = models.ForeignKey( on_delete=models.CASCADE, to="core.Board", db_index=True, - related_name="slug", - verbose_name="board", + verbose_name="board slug", ) permission: int = models.SmallIntegerField( verbose_name="permission", diff --git a/apps/core/permissions/article.py b/apps/core/permissions/article.py index d1d9a2bd..a70001bb 100644 --- a/apps/core/permissions/article.py +++ b/apps/core/permissions/article.py @@ -1,7 +1,6 @@ from rest_framework import permissions from apps.core.models import Article -from apps.core.models.board import BoardAccessPermissionType class ArticlePermission(permissions.IsAuthenticated): diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 8e73fa2e..d5289d59 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -6,7 +6,7 @@ from apps.core.documents import ArticleDocument from apps.core.models import Article, ArticleHiddenReason, Block, Board, Comment, Scrap -from apps.core.models.board import BoardAccessPermissionType, NameType +from apps.core.models.board import NameType from apps.core.serializers.attachment import AttachmentSerializer from apps.core.serializers.board import BoardSerializer from apps.core.serializers.mixins.hidden import ( diff --git a/apps/core/serializers/board.py b/apps/core/serializers/board.py index 84483c18..9f0f7c17 100644 --- a/apps/core/serializers/board.py +++ b/apps/core/serializers/board.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from apps.core.models.board import Board, BoardAccessPermissionType +from apps.core.models.board import Board from ara.classes.serializers import MetaDataModelSerializer diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 36b76100..7df9f961 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -11,7 +11,7 @@ from apps.core.filters.comment import CommentFilter from apps.core.models import Article, Comment, CommentDeleteLog, UserProfile, Vote -from apps.core.models.board import BoardAccessPermissionType, NameType +from apps.core.models.board import NameType from apps.core.permissions.comment import CommentPermission from apps.core.serializers.comment import ( CommentCreateActionSerializer, diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index 23eb1f7c..76142df6 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -7,10 +7,11 @@ from django.utils.functional import cached_property from django.utils.translation import gettext_lazy -from apps.user.models import UserGroup from ara.db.models import MetaDataModel from ara.settings import MIN_TIME +from .usergroup import UserGroup + class UserProfile(MetaDataModel): class Meta(MetaDataModel.Meta): From e9e2cb9c4937b6d97bebefd9641df25c1b5cb6b4 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 14:08:14 +0000 Subject: [PATCH 08/36] fix(permission): Remove deleted column --- apps/user/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/user/admin.py b/apps/user/admin.py index 5e6a9ebb..b0673cf6 100644 --- a/apps/user/admin.py +++ b/apps/user/admin.py @@ -31,7 +31,6 @@ class ManualUserAdmin(MetaDataModelAdmin): list_display = ( "user", "org_name", - "org_type", "applicant_name", "sso_email", ) From 3ae56e4bda26f62956d0070243a0131c2502c82a Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 14:08:48 +0000 Subject: [PATCH 09/36] fix(permission): Fix unique_together column name --- apps/user/models/usergroup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py index c3a7c9ff..31a91bd8 100644 --- a/apps/user/models/usergroup.py +++ b/apps/user/models/usergroup.py @@ -6,7 +6,7 @@ class UserGroup(models.Model): class Meta: verbose_name = "사용자 그룹" verbose_name_plural = "사용자가 속한 그룹 목록" - unique_together = (("userid", "groupid"),) + unique_together = (("user_id", "group_id"),) user_id = models.ForeignKey( verbose_name="사용자", From d7f119ad5280c76eb789c96a678b4eba9ef7d00e Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 15:22:53 +0000 Subject: [PATCH 10/36] refactor(permission): remove _id names and fix some of type hints --- apps/core/models/board_permission.py | 12 ++++++------ apps/user/models/usergroup.py | 14 ++++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/core/models/board_permission.py b/apps/core/models/board_permission.py index a4b4b237..9b925b0f 100644 --- a/apps/core/models/board_permission.py +++ b/apps/core/models/board_permission.py @@ -57,15 +57,15 @@ class BoardPermission(models.Model): class Meta: verbose_name = "BoardPermission" verbose_name_plural = "BoardPermissions" - unique_together = (("group_id", "board_slug", "permission"),) + unique_together = (("group", "board", "permission"),) - group_id = models.ForeignKey( + group = models.ForeignKey( on_delete=models.CASCADE, to="user.Group", db_index=True, verbose_name="group", ) - board_slug = models.ForeignKey( + board = models.ForeignKey( on_delete=models.CASCADE, to="core.Board", db_index=True, @@ -77,9 +77,9 @@ class Meta: ) @staticmethod - def permission_list_by_group(group: Group, board: Board) -> bool: + def permission_list_by_group(group: Group, board: Board) -> BoardAccessPermission: permissions = BoardAccessPermission(group, board) - groupPerms = BoardPermission.objects.filter(groupid=group, boardid=board) + groupPerms = BoardPermission.objects.filter(group=group, board=board) for perm in groupPerms: permissions.setPermission(perm.permission) @@ -92,7 +92,7 @@ def permission_list_by_user( groups = user.groups permissions = BoardAccessPermission(user, board) for group in groups: - groupPerms = BoardPermission.objects.filter(groupid=group, boardid=board) + groupPerms = BoardPermission.objects.filter(group=group, board=board) for perm in groupPerms: permissions.setPermission(perm.permission) diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py index 31a91bd8..d1717376 100644 --- a/apps/user/models/usergroup.py +++ b/apps/user/models/usergroup.py @@ -1,6 +1,8 @@ from django.conf import settings from django.db import models +from apps.user.models import Group, UserProfile + class UserGroup(models.Model): class Meta: @@ -8,7 +10,7 @@ class Meta: verbose_name_plural = "사용자가 속한 그룹 목록" unique_together = (("user_id", "group_id"),) - user_id = models.ForeignKey( + user = models.ForeignKey( verbose_name="사용자", on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL, @@ -16,7 +18,7 @@ class Meta: db_index=True, ) - group_id = models.ForeignKey( + group = models.ForeignKey( verbose_name="그룹", on_delete=models.CASCADE, to="user.Group", @@ -24,9 +26,9 @@ class Meta: ) @staticmethod - def search_by_user(self, user: int) -> list: - return UserGroup.objects.filter(user_id=user) + def search_by_user(self, user: UserProfile): + return UserGroup.objects.filter(user=user) @staticmethod - def search_by_group(self, group: int) -> list: # WARNING: Too many results - return UserGroup.objects.filter(group_id=group) + def search_by_group(self, group: Group): # WARNING: Too many results + return UserGroup.objects.filter(group=group) From 463298460868a7e46b35d9a0e1b0a6f139ab9706 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 15:30:48 +0000 Subject: [PATCH 11/36] refactor(permission): Fix type hinting --- apps/user/models/group.py | 2 +- apps/user/models/user_profile.py | 3 ++- apps/user/models/usergroup.py | 16 ++++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/user/models/group.py b/apps/user/models/group.py index bb51cada..888eb56b 100644 --- a/apps/user/models/group.py +++ b/apps/user/models/group.py @@ -32,7 +32,7 @@ def __str__(self) -> str: return self.name @staticmethod - def search_by_name(name: str) -> list: + def search_by_name(name: str) -> list["Group"]: return Group.objects.filter(name=name) @staticmethod diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index 76142df6..8e592b10 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -7,6 +7,7 @@ from django.utils.functional import cached_property from django.utils.translation import gettext_lazy +from apps.user.models import Group from ara.db.models import MetaDataModel from ara.settings import MIN_TIME @@ -148,5 +149,5 @@ def is_school_admin(self) -> bool: return self.group == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN @cached_property - def groups(self) -> list: + def groups(self) -> list[Group]: return UserGroup.search_by_user(self.user) diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py index d1717376..cd1af235 100644 --- a/apps/user/models/usergroup.py +++ b/apps/user/models/usergroup.py @@ -26,9 +26,17 @@ class Meta: ) @staticmethod - def search_by_user(self, user: UserProfile): - return UserGroup.objects.filter(user=user) + def search_by_user(self, user: UserProfile) -> list[Group]: + groups = [] + for usergroup in UserGroup.objects.filter(user=user): + groups.append(usergroup.group) + return groups @staticmethod - def search_by_group(self, group: Group): # WARNING: Too many results - return UserGroup.objects.filter(group=group) + def search_by_group( + self, group: Group + ) -> list[UserProfile]: # WARNING: Too many results + users = [] + for usergroup in UserGroup.objects.filter(group=group): + users.append(usergroup.user) + return users From 706cf12008dcc706687f483479cb6ef53d2299b7 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 15:33:05 +0000 Subject: [PATCH 12/36] refactor(permission): add annotation --- apps/user/models/usergroup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py index cd1af235..e0f3e7cb 100644 --- a/apps/user/models/usergroup.py +++ b/apps/user/models/usergroup.py @@ -1,7 +1,12 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.conf import settings from django.db import models -from apps.user.models import Group, UserProfile +if TYPE_CHECKING: + from apps.user.models import Group, UserProfile class UserGroup(models.Model): From e4b7544a018b2611d6a209cd207ac492bd98e3cc Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 9 Jan 2024 15:38:59 +0000 Subject: [PATCH 13/36] feat(permission): Remove all bitmasks --- apps/core/models/board.py | 48 ++++++++++++--------------------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 9352009d..1de0a298 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -35,24 +35,6 @@ class Board(MetaDataModel): verbose_name="게시판 영문 이름", max_length=32, ) - # 사용자 그룹에 대해 접근 권한을 제어하는 bit mask 입니다. - # access_mask & (1 << user.group) > 0 일 때 접근이 가능합니다. - # 사용자 그룹의 값들은 `UserGroup`을 참고하세요. - read_access_mask = models.SmallIntegerField( - # UNAUTHORIZED, EXTERNAL_ORG 제외 모든 사용자 읽기 권한 부여 - verbose_name="읽기 권한", - default=0b011011110, - ) - write_access_mask = models.SmallIntegerField( - # UNAUTHORIZED, STORE_EMPLOYEE, EXTERNAL_ORG 제외 모든 사용자 쓰기 권한 부여 - verbose_name="쓰기 권한", - default=0b011011010, - ) - comment_access_mask = models.SmallIntegerField( - # UNAUTHORIZED 제외 모든 사용자 댓글 권한 부여 - verbose_name="댓글 권한", - default=0b011111110, - ) is_readonly = models.BooleanField( verbose_name="읽기 전용 게시판", default=False, @@ -122,18 +104,18 @@ def permission_list_by_group(self, group: Group) -> BoardAccessPermission: def permission_list_by_user(self, user: UserProfile) -> BoardAccessPermission: return BoardPermission.permission_list_by_user(self, user) - def old_group_has_access_permission( - self, access_type: OldBoardAccessPermissionType, group: int - ) -> bool: - mask = None - if access_type == OldBoardAccessPermissionType.READ: - mask = self.read_access_mask - elif access_type == OldBoardAccessPermissionType.WRITE: - mask = self.write_access_mask - elif access_type == OldBoardAccessPermissionType.COMMENT: - mask = self.comment_access_mask - else: - # TODO: Handle error - return False - - return (mask & (1 << group)) > 0 + # def old_group_has_access_permission( + # self, access_type: OldBoardAccessPermissionType, group: int + # ) -> bool: + # mask = None + # if access_type == OldBoardAccessPermissionType.READ: + # mask = self.read_access_mask + # elif access_type == OldBoardAccessPermissionType.WRITE: + # mask = self.write_access_mask + # elif access_type == OldBoardAccessPermissionType.COMMENT: + # mask = self.comment_access_mask + # else: + # # TODO: Handle error + # return False + # + # return (mask & (1 << group)) > 0 From 7d0861c5e76f68e204e8fc54e46d354d1f78210a Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Wed, 10 Jan 2024 17:24:07 +0000 Subject: [PATCH 14/36] fix(permission): Fix UserGroup PK --- apps/user/models/usergroup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py index e0f3e7cb..b60d8851 100644 --- a/apps/user/models/usergroup.py +++ b/apps/user/models/usergroup.py @@ -13,13 +13,12 @@ class UserGroup(models.Model): class Meta: verbose_name = "사용자 그룹" verbose_name_plural = "사용자가 속한 그룹 목록" - unique_together = (("user_id", "group_id"),) + unique_together = (("user", "group"),) user = models.ForeignKey( verbose_name="사용자", on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL, - primary_key=True, db_index=True, ) From 49fb8f6bcc6997825459d896074f236bd7d9fced Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Wed, 10 Jan 2024 17:28:35 +0000 Subject: [PATCH 15/36] fix(permission): Resotre org_type on manual --- apps/user/models/user/manual.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/user/models/user/manual.py b/apps/user/models/user/manual.py index 72512fc9..819e8d5f 100644 --- a/apps/user/models/user/manual.py +++ b/apps/user/models/user/manual.py @@ -25,6 +25,14 @@ class Meta(MetaDataModel.Meta): verbose_name="업체/단체 이름", ) + org_type: Group = models.ForeignKey( + on_delete=models.CASCADE, + to="user.Group", + verbose_name="업체/단체 그룹", + null=True, + blank=True, + ) + applicant_name = models.CharField( max_length=160, verbose_name="신청자 이름", From 33546bb5c1b5fadcaedb2c19d44623ba269a7968 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Wed, 10 Jan 2024 17:30:11 +0000 Subject: [PATCH 16/36] fix(permission): Remove every OldUserGroup --- apps/core/models/board_permission.py | 4 +- apps/core/models/signals/comment.py | 5 +- .../core/permissions/communication_article.py | 8 ++-- apps/core/views/viewsets/comment.py | 7 ++- apps/user/models/user_profile.py | 45 ++++++++++++++++-- apps/user/views/viewsets/user.py | 15 +++--- tests/conftest.py | 47 ++++++++++++++----- tests/test_articles.py | 27 +++++++---- 8 files changed, 110 insertions(+), 48 deletions(-) diff --git a/apps/core/models/board_permission.py b/apps/core/models/board_permission.py index 9b925b0f..100fb163 100644 --- a/apps/core/models/board_permission.py +++ b/apps/core/models/board_permission.py @@ -22,9 +22,7 @@ class BoardAccessPermissionType(IntEnum): class BoardAccessPermission: - def __init__( - self, target: UserProfile | Group, board: Board - ) -> None: # permission: [BoardAccessPermissionType] + def __init__(self, target: UserProfile | Group, board: Board) -> None: self.target = target self.board = board self.READ = False diff --git a/apps/core/models/signals/comment.py b/apps/core/models/signals/comment.py index d67bd93c..5cbfe4d2 100644 --- a/apps/core/models/signals/comment.py +++ b/apps/core/models/signals/comment.py @@ -33,8 +33,9 @@ def update_communication_article_status(comment): ) if ( article.parent_board.is_school_communication - and comment.created_by.profile.group - == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN + and comment.created_by.profile.has_group_by_id( + 7 + ) # 7 = Communication board admin and article.communication_article.school_response_status != SchoolResponseStatus.ANSWER_DONE ): diff --git a/apps/core/permissions/communication_article.py b/apps/core/permissions/communication_article.py index 0d67bb3c..07db0477 100644 --- a/apps/core/permissions/communication_article.py +++ b/apps/core/permissions/communication_article.py @@ -7,8 +7,6 @@ class CommunicationArticleAdminPermission(permissions.IsAuthenticated): message = "You are not authorized to access this feature" def has_permission(self, request, view): - return ( - request.user.is_staff - or request.user.profile.group - == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN - ) + return request.user.is_staff or request.user.profile.has_group_by_id( + 7 + ) # 7 = Communication board admin diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 7df9f961..4fdba5bf 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -87,10 +87,9 @@ def perform_create(self, serializer): print(parent_article) created_by = self.request.user - is_school_admin = ( - UserProfile.objects.get(user_id=created_by).group - == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN - ) + is_school_admin = UserProfile.objects.get(user_id=created_by).has_group_by_id( + 7 + ) # 7 = Communication board admin if is_school_admin and parent_article.name_type != NameType.ANONYMOUS: name_type = NameType.REGULAR diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index 8e592b10..c5a8fe34 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -139,15 +139,50 @@ def realname(self) -> str: @cached_property def is_official(self) -> bool: - return ( - self.group in UserProfile.OFFICIAL_GROUPS - or self.user.email == "new-ara@sparcs.org" - ) + if self.user.email == "new-ara@sparcs.org": + return True + for group in self.groups: + if group.is_official: + return True + return False @cached_property def is_school_admin(self) -> bool: - return self.group == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN + return self.has_group_by_id(7) # 7 = Communication board admin @cached_property def groups(self) -> list[Group]: return UserGroup.search_by_user(self.user) + + def has_group(self, group: Group) -> bool: + return group in self.groups + + def has_group_by_name(self, name: str) -> bool: + return any([group.name == name for group in self.groups]) + + def has_group_by_id(self, group_id: int) -> bool: + return any([group.group_id == group_id for group in self.groups]) + + def add_group(self, group: Group) -> None: + if not self.has_group(group): + UserGroup.objects.create(user=self.user, group=group) + + def add_group_by_name(self, name: str) -> None: + group = Group.search_by_name(name) + self.add_group(group) + + def add_group_by_id(self, group_id: int) -> None: + group = Group.search_by_id(group_id) + self.add_group(group) + + def remove_group(self, group: Group) -> None: + if self.has_group(group): + UserGroup.objects.get(user=self.user, group=group).delete() + + def remove_group_by_name(self, name: str) -> None: + group = Group.search_by_name(name) + self.remove_group(group) + + def remove_group_by_id(self, group_id: int) -> None: + group = Group.search_by_id(group_id) + self.remove_group(group) diff --git a/apps/user/views/viewsets/user.py b/apps/user/views/viewsets/user.py index 0bbeef0d..f09ac60b 100644 --- a/apps/user/views/viewsets/user.py +++ b/apps/user/views/viewsets/user.py @@ -13,7 +13,7 @@ from django.utils.functional import cached_property from rest_framework import decorators, permissions, response, status -from apps.user.models import UserProfile +from apps.user.models import Group, UserGroup, UserProfile from apps.user.models.user.manual import ManualUser from apps.user.permissions.user import UserPermission from ara.classes.sparcssso import Client as SSOClient @@ -232,10 +232,12 @@ def sso_login_callback(self, request, *args, **kwargs): # 2. 아직 승인 이전, 회원가입을 시도했던 공용 계정 회원 if (not user_profile.user.is_active) and (is_kaist or is_manual): user_profile.user.is_active = True + if user_profile.has_group_by_id(1): # 1 = UNAUTHORIZED + user_profile.remove_group_by_id(1) if is_manual: - user_profile.group = manual_user.org_type + user_profile.add_group(manual_user.org_type) elif is_kaist: - user_profile.group = UserProfile.UserGroup.KAIST_MEMBER + user_profile.add_group_by_id(2) # 2 = KAIST_MEMBER user_profile.sso_user_info = user_info user_profile.save() @@ -258,7 +260,7 @@ def sso_login_callback(self, request, *args, **kwargs): password=str(uuid.uuid4()), is_active=is_kaist or is_manual, ) - user_group = UserProfile.UserGroup.UNAUTHORIZED + user_group = Group.search_by_id(1) # 1 = UNAUTHORIZED if is_manual: manual_user.user = new_user @@ -268,7 +270,7 @@ def sso_login_callback(self, request, *args, **kwargs): user_group = manual_user.org_type elif is_kaist: - user_group = UserProfile.UserGroup.KAIST_MEMBER + user_group = Group.search_by_id(2) # 2 = KAIST_MEMBER user_profile = UserProfile.objects.create( uid=user_info["uid"], @@ -276,10 +278,11 @@ def sso_login_callback(self, request, *args, **kwargs): nickname=user_nickname, sso_user_info=user_info, user=new_user, - group=user_group, picture=user_profile_picture, ) + UserGroup.objects.create(user=new_user, group=user_group) + if not user_profile.user.is_active: return redirect( to=reverse("core:InvalidSsoLoginView") diff --git a/tests/conftest.py b/tests/conftest.py index 7ab8a049..74ea7c7c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ from django.utils import timezone from rest_framework.test import APIClient -from apps.user.models import UserProfile +from apps.user.models import Group, UserGroup, UserProfile from ara import redis User = get_user_model() @@ -40,7 +40,6 @@ def set_user_client(request): UserProfile.objects.get_or_create( user=request.cls.user, nickname="User", - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', @@ -48,6 +47,10 @@ def set_user_client(request): "last_name": "LastName", }, ) + UserGroup.objects.create( + user=request.cls.user, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) client = APIClient() request.cls.api_client = client @@ -61,7 +64,6 @@ def set_user_client2(request): UserProfile.objects.get_or_create( user=request.cls.user2, nickname="User2", - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', @@ -69,6 +71,10 @@ def set_user_client2(request): "last_name": "LastName", }, ) + UserGroup.objects.create( + user=request.cls.user2, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) request.cls.api_client = APIClient() @@ -81,7 +87,6 @@ def set_user_client3(request): UserProfile.objects.get_or_create( user=request.cls.user3, nickname="User3", - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', @@ -89,6 +94,10 @@ def set_user_client3(request): "last_name": "LastName", }, ) + UserGroup.objects.create( + user=request.cls.user3, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) request.cls.api_client = APIClient() @@ -102,7 +111,6 @@ def set_user_client4(request): UserProfile.objects.get_or_create( user=request.cls.user4, nickname="User4", - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', @@ -110,6 +118,10 @@ def set_user_client4(request): "last_name": "LastName", }, ) + UserGroup.objects.create( + user=request.cls.user4, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) request.cls.api_client = APIClient() @@ -124,9 +136,12 @@ def set_user_with_kaist_info(request): user=request.cls.user_with_kaist_info, nickname="user_with_kinfo", sso_user_info={"kaist_info": '{"ku_kname": "user_with_kaist_info"}'}, - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), ) + UserGroup.objects.create( + user=request.cls.user_with_kaist_info, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) request.cls.api_client = APIClient() @@ -145,9 +160,12 @@ def set_user_without_kaist_info(request): "last_name": "user_", "first_name": "without_kaist_info", }, - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), ) + UserGroup.objects.create( + user=request.cls.user_without_kaist_info, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) request.cls.api_client = APIClient() @@ -162,13 +180,16 @@ def set_school_admin(request): user=request.cls.school_admin, nickname="School Admin", agree_terms_of_service_at=timezone.now(), - group=UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN, sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', "first_name": "FirstName", "last_name": "LastName", }, ) + UserGroup.objects.create( + user=request.cls.school_admin, + group=Group.search_by_id(7), # 7 = Communication board admin + ) request.cls.api_client = APIClient() @@ -205,14 +226,13 @@ def create_user( username: str = "User", email: str = "user@sparcs.org", nickname: str = "Nickname", - group: UserProfile.UserGroup = UserProfile.UserGroup.KAIST_MEMBER, + group: Group = Group.search_by_id(2), # 2 = KAIST_MEMBER ) -> User: user, created = User.objects.get_or_create(username=username, email=email) if created: UserProfile.objects.create( user=user, nickname=nickname, - group=group, agree_terms_of_service_at=timezone.now(), sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', @@ -220,22 +240,23 @@ def create_user( "last_name": f"Lastname", }, ) + UserGroup.objects.create(user=user, group=group) return user @classmethod - def create_user_with_index(cls, idx: int, group: UserProfile.UserGroup) -> User: + def create_user_with_index(cls, idx: int, group: Group) -> User: user = cls.create_user( username=f"User{idx}", email=f"user{idx}@sparcs.org", nickname=f"Nickname{idx}", - group=group, ) + UserGroup.objects.create(user=user, group=group) return user @classmethod def create_users( cls, num: int, - group: UserProfile.UserGroup = UserProfile.UserGroup.KAIST_MEMBER, + group: Group = Group.search_by_id(2), # 2 = KAIST_MEMBER ) -> list[User]: return [cls.create_user_with_index(idx, group) for idx in range(num)] diff --git a/tests/test_articles.py b/tests/test_articles.py index 6353065e..f39dd757 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -4,8 +4,8 @@ from rest_framework import status from apps.core.models import Article, Block, Board, Comment, Topic, Vote -from apps.core.models.board import BoardAccessPermissionType, NameType -from apps.user.models import UserProfile +from apps.core.models.board import NameType +from apps.user.models import Group, UserGroup, UserProfile from ara.settings import MIN_TIME, SCHOOL_RESPONSE_VOTE_THRESHOLD from tests.conftest import RequestSetting, TestCase, Utils @@ -173,9 +173,12 @@ def set_kaist_articles(request): user=request.cls.kaist_user, nickname="KAIST User", agree_terms_of_service_at=timezone.now(), - group=UserProfile.UserGroup.KAIST_MEMBER, sso_user_info={}, ) + UserGroup.objects.get_or_create( + user=request.cls.kaist_user, + group=Group.search_by_id(2), # 2 = KAIST member + ) request.cls.kaist_board, _ = Board.objects.get_or_create( slug="kaist-only", @@ -328,10 +331,11 @@ def test_create(self): # get request 시 user의 read 권한 확인 테스트 def test_check_read_permission_when_get(self): group_users = [] - for idx, group in enumerate(UserProfile.UserGroup): - user = Utils.create_user_with_index(idx, group) + for group in Group.objects.all(): + user = Utils.create_user_with_index(group.group_id, group) group_users.append(user) - assert len(group_users) == len(UserProfile.UserGroup) + + assert len(group_users) == len(Group.objects.all()) articles = [self.regular_access_article, self.advertiser_accessible_article] @@ -348,10 +352,10 @@ def test_check_read_permission_when_get(self): # create 단계에서 user의 write 권한 확인 테스트 def test_check_write_permission_when_create(self): group_users = [] - for idx, group in enumerate(UserProfile.UserGroup): - user = Utils.create_user_with_index(idx, group) + for group in Group.objects.all(): + user = Utils.create_user_with_index(group.group_id, group) group_users.append(user) - assert len(group_users) == len(UserProfile.UserGroup) + assert len(group_users) == len(Group.objects.all()) boards = [ self.regular_access_board, @@ -904,10 +908,13 @@ def _user_factory(user_kwargs, profile_kwargs): **profile_kwargs, "user": user_instance, "agree_terms_of_service_at": timezone.now(), - "group": UserProfile.UserGroup.KAIST_MEMBER, "sso_user_info": {}, } ) + UserGroup.objects.get_or_create( + user=user_instance, + group=Group.search_by_id(2), # 2 = KAIST member + ) return user_instance @classmethod From 226d1d82e62b261595c519f7f054996d06aad2db Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Wed, 10 Jan 2024 18:04:57 +0000 Subject: [PATCH 17/36] fix(permission): Fix BoardPermission legacy codes --- apps/core/models/board_permission.py | 105 ++++++++++++++++++++++++++- tests/test_articles.py | 96 +++++++++++++++++++++--- tests/test_communication_article.py | 30 +++++++- 3 files changed, 218 insertions(+), 13 deletions(-) diff --git a/apps/core/models/board_permission.py b/apps/core/models/board_permission.py index 100fb163..17062086 100644 --- a/apps/core/models/board_permission.py +++ b/apps/core/models/board_permission.py @@ -21,9 +21,36 @@ class BoardAccessPermissionType(IntEnum): DELETE = 4 +DEFAULT_READ_PERMISSION: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.READ), + (3, BoardAccessPermissionType.READ), + (4, BoardAccessPermissionType.READ), + (5, BoardAccessPermissionType.READ), + (7, BoardAccessPermissionType.READ), + (8, BoardAccessPermissionType.READ), +] +DEFAULT_WRITE_PERMISSION: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.WRITE), + (4, BoardAccessPermissionType.WRITE), + (5, BoardAccessPermissionType.WRITE), + (7, BoardAccessPermissionType.WRITE), + (8, BoardAccessPermissionType.WRITE), +] +DEFAULT_COMMENT_PERMISSION: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.COMMENT), + (3, BoardAccessPermissionType.COMMENT), + (4, BoardAccessPermissionType.COMMENT), + (5, BoardAccessPermissionType.COMMENT), + (6, BoardAccessPermissionType.COMMENT), + (7, BoardAccessPermissionType.COMMENT), + (8, BoardAccessPermissionType.COMMENT), +] + + class BoardAccessPermission: def __init__(self, target: UserProfile | Group, board: Board) -> None: - self.target = target + self.user = target if isinstance(target, UserProfile) else None + self.group = target if isinstance(target, Group) else None self.board = board self.READ = False self.WRITE = False @@ -95,3 +122,79 @@ def permission_list_by_user( permissions.setPermission(perm.permission) return permissions + + @staticmethod + def add_permission( + group: Group, + board: Board, + permission: BoardAccessPermissionType, + ): + BoardPermission.objects.create( + group=group, + board=board, + permission=permission, + ) + + @staticmethod + def remove_permission( + group: Group, + board: Board, + permission: BoardAccessPermissionType, + ): + BoardPermission.objects.filter( + group=group, + board=board, + permission=permission, + ).delete() + + @staticmethod + def add_permission_bulk_by_board( + board: Board, + perms: list[tuple[int, BoardAccessPermissionType]], + ): + for group_id, perm in perms: + BoardPermission.objects.create( + group=Group.search_by_id(group_id), + board=board, + permission=perm, + ) + + @staticmethod + def set_group_permission(permission: BoardAccessPermission): + if permission.group is None: + # raise ValueError("permission.group is None") + return + BoardPermission.objects.filter( + group=permission.group, board=permission.board + ).delete() + if permission.DENY: + BoardPermission.objects.create( + group=permission.group, + board=permission.board, + permission=BoardAccessPermissionType.DENY, + ) + return + if permission.READ: + BoardPermission.objects.create( + group=permission.group, + board=permission.board, + permission=BoardAccessPermissionType.READ, + ) + if permission.WRITE: + BoardPermission.objects.create( + group=permission.group, + board=permission.board, + permission=BoardAccessPermissionType.WRITE, + ) + if permission.COMMENT: + BoardPermission.objects.create( + group=permission.group, + board=permission.board, + permission=BoardAccessPermissionType.COMMENT, + ) + if permission.DELETE: + BoardPermission.objects.create( + group=permission.group, + board=permission.board, + permission=BoardAccessPermissionType.DELETE, + ) diff --git a/tests/test_articles.py b/tests/test_articles.py index f39dd757..5c9978d2 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -5,6 +5,13 @@ from apps.core.models import Article, Block, Board, Comment, Topic, Vote from apps.core.models.board import NameType +from apps.core.models.board_permission import ( + DEFAULT_COMMENT_PERMISSION, + DEFAULT_READ_PERMISSION, + DEFAULT_WRITE_PERMISSION, + BoardAccessPermissionType, + BoardPermission, +) from apps.user.models import Group, UserGroup, UserProfile from ara.settings import MIN_TIME, SCHOOL_RESPONSE_VOTE_THRESHOLD from tests.conftest import RequestSetting, TestCase, Utils @@ -44,8 +51,25 @@ def set_boards(request): slug="regular access", ko_name="일반 접근 권한 게시판", en_name="Regular Access Board", - read_access_mask=0b11011110, - write_access_mask=0b11011010, + # read_access_mask=0b11011110, + # write_access_mask=0b11011010, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.READ), + (3, BoardAccessPermissionType.READ), + (4, BoardAccessPermissionType.READ), + (5, BoardAccessPermissionType.READ), + (7, BoardAccessPermissionType.READ), + (8, BoardAccessPermissionType.READ), + (2, BoardAccessPermissionType.WRITE), + (4, BoardAccessPermissionType.WRITE), + (5, BoardAccessPermissionType.WRITE), + (7, BoardAccessPermissionType.WRITE), + (8, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.regular_access_board, permission_bulk ) # Though its name is 'advertiser accessible', enterprise is also accessible @@ -53,29 +77,74 @@ def set_boards(request): slug="advertiser accessible", ko_name="외부인(홍보 계정) 접근 가능 게시판", en_name="Advertiser Accessible Board", - read_access_mask=0b11111110, - write_access_mask=0b11111110, + # read_access_mask=0b11111110, + # write_access_mask=0b11111110, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.READ), + (3, BoardAccessPermissionType.READ), + (4, BoardAccessPermissionType.READ), + (5, BoardAccessPermissionType.READ), + (7, BoardAccessPermissionType.READ), + (8, BoardAccessPermissionType.READ), + (2, BoardAccessPermissionType.WRITE), + (4, BoardAccessPermissionType.WRITE), + (5, BoardAccessPermissionType.WRITE), + (6, BoardAccessPermissionType.WRITE), + (7, BoardAccessPermissionType.WRITE), + (8, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.advertiser_accessible_board, permission_bulk ) request.cls.nonwritable_board = Board.objects.create( slug="nonwritable", ko_name="글 작성 불가 게시판", en_name="Nonwritable Board", - write_access_mask=0b00000000, + # write_access_mask=0b00000000, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [] + permission_bulk.extend(DEFAULT_READ_PERMISSION) + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.nonwritable_board, permission_bulk ) request.cls.newsadmin_writable_board = Board.objects.create( slug="newsadmin writable", ko_name="뉴스게시판 관리인 글 작성 가능 게시판", en_name="Newsadmin Writable Board", - write_access_mask=0b10000000, + # write_access_mask=0b10000000, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (8, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_READ_PERMISSION) + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.newsadmin_writable_board, permission_bulk ) request.cls.enterprise_writable_board = Board.objects.create( slug="enterprise writable", ko_name="입주업체 글 작성 가능 게시판", en_name="Enterprise Writable Board", - write_access_mask=0b11011110, + # write_access_mask=0b11011110, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.WRITE), + (3, BoardAccessPermissionType.WRITE), + (4, BoardAccessPermissionType.WRITE), + (5, BoardAccessPermissionType.WRITE), + (7, BoardAccessPermissionType.WRITE), + (8, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_READ_PERMISSION) + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.enterprise_writable_board, permission_bulk ) @@ -184,9 +253,18 @@ def set_kaist_articles(request): slug="kaist-only", ko_name="KAIST Board", en_name="KAIST Board", - read_access_mask=0b00000010, - write_access_mask=0b00000010, + # read_access_mask=0b00000010, + # write_access_mask=0b00000010, ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.READ), + (2, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.kaist_board, permission_bulk + ) + request.cls.kaist_article, _ = Article.objects.get_or_create( title="example article", content="example content", diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index ffbc4a1b..e8b5ebd1 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -6,8 +6,15 @@ from django.utils import timezone from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST -from apps.core.models import Article, Board +from apps.core.models import Article, Board, BoardAccessPermissionType, BoardPermission from apps.core.models.board import NameType +from apps.core.models.board_permission import ( + DEFAULT_COMMENT_PERMISSION, + DEFAULT_READ_PERMISSION, + DEFAULT_WRITE_PERMISSION, + BoardAccessPermissionType, + BoardPermission, +) from apps.core.models.communication_article import ( CommunicationArticle, SchoolResponseStatus, @@ -26,8 +33,25 @@ def set_communication_board(request): en_name="With School (Test)", is_school_communication=True, name_type=NameType.REALNAME, - read_access_mask=0b11011110, - write_access_mask=0b11011010, + # read_access_mask=0b11011110, + # write_access_mask=0b11011010, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.READ), + (3, BoardAccessPermissionType.READ), + (4, BoardAccessPermissionType.READ), + (5, BoardAccessPermissionType.READ), + (7, BoardAccessPermissionType.READ), + (8, BoardAccessPermissionType.READ), + (2, BoardAccessPermissionType.WRITE), + (4, BoardAccessPermissionType.WRITE), + (5, BoardAccessPermissionType.WRITE), + (7, BoardAccessPermissionType.WRITE), + (8, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.communication_board, permission_bulk ) From cdc2effda76442d8e8b5d17baf63bb78246b3179 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Wed, 10 Jan 2024 18:32:47 +0000 Subject: [PATCH 18/36] fix(permission): Add default groups on test --- tests/conftest.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 74ea7c7c..33c62e31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -260,3 +260,24 @@ def create_users( group: Group = Group.search_by_id(2), # 2 = KAIST_MEMBER ) -> list[User]: return [cls.create_user_with_index(idx, group) for idx in range(num)] + + @classmethod + def add_default_groups(cls): + default_groups = { + 1: ("Unauthorized user", "뉴아라 계정을 만들지 않은 사람들", False), + 2: ("KAIST member", "카이스트 메일을 가진 사람 (학생, 교직원)", False), + 3: ("Store employee", "교내 입주 업체 직원", False), + 4: ("Other member", "카이스트 메일이 없는 개인 (특수한 관련자 등)", False), + 5: ("KAIST organization", "교내 학생 단체들", True), + 6: ("External organization", "외부인 (홍보 계정 등)", True), + 7: ("Communication board admin", "소통게시판 관리인", False), + 8: ("News board admin", "뉴스게시판 관리인", False), + } + + for group_id, (name, description, is_official) in default_groups.items(): + Group.objects.create( + group_id=group_id, + name=name, + description=description, + is_official=is_official, + ) From 5708cff2cb813c3159e919fd7f623accf5b15ff7 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Wed, 10 Jan 2024 18:35:38 +0000 Subject: [PATCH 19/36] feat(permission): Migration file --- ...move_board_comment_access_mask_and_more.py | 99 ++++++++++++ ...group_remove_userprofile_group_and_more.py | 143 ++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 apps/core/migrations/0058_remove_board_comment_access_mask_and_more.py create mode 100644 apps/user/migrations/0022_group_remove_userprofile_group_and_more.py diff --git a/apps/core/migrations/0058_remove_board_comment_access_mask_and_more.py b/apps/core/migrations/0058_remove_board_comment_access_mask_and_more.py new file mode 100644 index 00000000..31944323 --- /dev/null +++ b/apps/core/migrations/0058_remove_board_comment_access_mask_and_more.py @@ -0,0 +1,99 @@ +# Generated by Django 4.2.3 on 2024-01-10 17:16 + +import django.db.models.deletion +from django.db import migrations, models + +from apps.core.models import BoardAccessPermissionType + + +def move_board_permissions(apps, schema_editor): + Board = apps.get_model("core", "Board") + BoardPermission = apps.get_model("core", "BoardPermission") + Group = apps.get_model("user", "Group") + for board in Board.objects.all(): + read_access_mask = board.read_access_mask + write_access_mask = board.write_access_mask + comment_access_mask = board.comment_access_mask + + for group in Group.objects.all(): + read = bool((read_access_mask & (1 << (group.group_id - 1))) > 0) + write = bool((write_access_mask & (1 << (group.group_id - 1))) > 0) + comment = bool((comment_access_mask & (1 << (group.group_id - 1))) > 0) + + if read: + BoardPermission.objects.create( + board=board, + group=group, + permission=BoardAccessPermissionType.READ, + ) + if write: + BoardPermission.objects.create( + board=board, + group=group, + permission=BoardAccessPermissionType.WRITE, + ) + if comment: + BoardPermission.objects.create( + board=board, + group=group, + permission=BoardAccessPermissionType.COMMENT, + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("user", "0022_group_remove_userprofile_group_and_more"), + ("core", "0057_alter_article_name_type_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="BoardPermission", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("permission", models.SmallIntegerField(verbose_name="permission")), + ( + "board", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.board", + verbose_name="board slug", + ), + ), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="user.group", + verbose_name="group", + ), + ), + ], + options={ + "verbose_name": "BoardPermission", + "verbose_name_plural": "BoardPermissions", + "unique_together": {("group", "board", "permission")}, + }, + ), + migrations.RunPython(move_board_permissions), + migrations.RemoveField( + model_name="board", + name="comment_access_mask", + ), + migrations.RemoveField( + model_name="board", + name="read_access_mask", + ), + migrations.RemoveField( + model_name="board", + name="write_access_mask", + ), + ] diff --git a/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py b/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py new file mode 100644 index 00000000..7ebaecdf --- /dev/null +++ b/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py @@ -0,0 +1,143 @@ +# Generated by Django 4.2.3 on 2024-01-10 17:16 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +def add_groups(apps, schema_editor): + default_groups = { + 1: ("Unauthorized user", "뉴아라 계정을 만들지 않은 사람들", False), + 2: ("KAIST member", "카이스트 메일을 가진 사람 (학생, 교직원)", False), + 3: ("Store employee", "교내 입주 업체 직원", False), + 4: ("Other member", "카이스트 메일이 없는 개인 (특수한 관련자 등)", False), + 5: ("KAIST organization", "교내 학생 단체들", True), + 6: ("External organization", "외부인 (홍보 계정 등)", True), + 7: ("Communication board admin", "소통게시판 관리인", False), + 8: ("News board admin", "뉴스게시판 관리인", False), + } + + Group = apps.get_model("user", "Group") + for group_id, (name, description, is_official) in default_groups.items(): + Group.objects.create( + group_id=group_id, + name=name, + description=description, + is_official=is_official, + ) + + +def add_user_groups(apps, schema_editor): + cursor = schema_editor.connection.cursor() + cursor.execute( + """ + INSERT INTO user_usergroup (user_id, group_id) + SELECT a.user_id, (a.group+1) FROM user_userprofile a, user_group b + WHERE (a.group+1) = b.group_id; + """ + ) + # schema_editor.connection.commit() + cursor.close() + + +def fix_manualuser_org_type(apps, schema_editor): + cursor = schema_editor.connection.cursor() + cursor.execute( + """ + UPDATE user_manualuser + SET org_type = org_type + 1; + """ + ) + cursor.close() + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("user", "0021_remove_userprofile_extra_preferences"), + ] + + operations = [ + migrations.CreateModel( + name="Group", + fields=[ + ( + "group_id", + models.AutoField( + db_index=True, + primary_key=True, + serialize=False, + verbose_name="Group ID", + ), + ), + ("name", models.CharField(max_length=32, verbose_name="Group name")), + ( + "description", + models.CharField( + max_length=128, null=True, verbose_name="Group description" + ), + ), + ( + "is_official", + models.BooleanField(default=False, verbose_name="공식 단체 또는 학생단체"), + ), + ], + options={ + "verbose_name": "Group", + "verbose_name_plural": "Groups", + }, + ), + migrations.CreateModel( + name="UserGroup", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="user.group", + verbose_name="그룹", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="사용자", + ), + ), + ], + options={ + "verbose_name": "사용자 그룹", + "verbose_name_plural": "사용자가 속한 그룹 목록", + "unique_together": {("user", "group")}, + }, + ), + migrations.RunPython(add_groups), + migrations.RunPython(add_user_groups), + migrations.RunPython(fix_manualuser_org_type), + migrations.RemoveField( + model_name="userprofile", + name="group", + ), + migrations.AlterField( + model_name="manualuser", + name="org_type", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="user.group", + verbose_name="업체/단체 그룹", + ), + ), + ] From 6c39c2463345870f2a9576cf450066745a4bc982 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Thu, 11 Jan 2024 06:51:06 +0000 Subject: [PATCH 20/36] fix(permission): Fix UserProfile to User --- apps/core/models/board.py | 17 ++++++++++++++--- apps/core/models/board_permission.py | 12 ++++++++---- apps/user/models/user_profile.py | 13 ++++++++----- apps/user/models/usergroup.py | 10 ++++------ 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 1de0a298..65fee4b9 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -7,7 +7,15 @@ from ara.db.models import MetaDataModel from .board_group import BoardGroup -from .board_permission import BoardAccessPermission, BoardPermission +from .board_permission import ( + DEFAULT_COMMENT_PERMISSION, + DEFAULT_PERMISSIONS, + DEFAULT_READ_PERMISSION, + DEFAULT_WRITE_PERMISSION, + BoardAccessPermission, + BoardAccessPermissionType, + BoardPermission, +) class NameType(IntFlag): @@ -99,10 +107,13 @@ def __str__(self) -> str: return self.ko_name def permission_list_by_group(self, group: Group) -> BoardAccessPermission: - return BoardPermission.permission_list_by_group(self, group) + return BoardPermission.permission_list_by_group(group, self) def permission_list_by_user(self, user: UserProfile) -> BoardAccessPermission: - return BoardPermission.permission_list_by_user(self, user) + return BoardPermission.permission_list_by_user(user, self) + + def set_default_permission(self): + BoardPermission.add_permission_bulk_by_board(self, DEFAULT_PERMISSIONS) # def old_group_has_access_permission( # self, access_type: OldBoardAccessPermissionType, group: int diff --git a/apps/core/models/board_permission.py b/apps/core/models/board_permission.py index 17062086..797b75ca 100644 --- a/apps/core/models/board_permission.py +++ b/apps/core/models/board_permission.py @@ -7,7 +7,7 @@ from django.db import models import apps.core.models.board as board -from apps.user.models import Group, UserProfile +from apps.user.models import Group, UserGroup, UserProfile if TYPE_CHECKING: from .board import Board @@ -45,6 +45,10 @@ class BoardAccessPermissionType(IntEnum): (7, BoardAccessPermissionType.COMMENT), (8, BoardAccessPermissionType.COMMENT), ] +DEFAULT_PERMISSIONS = [] +DEFAULT_PERMISSIONS.extend(DEFAULT_READ_PERMISSION) +DEFAULT_PERMISSIONS.extend(DEFAULT_WRITE_PERMISSION) +DEFAULT_PERMISSIONS.extend(DEFAULT_COMMENT_PERMISSION) class BoardAccessPermission: @@ -112,9 +116,9 @@ def permission_list_by_group(group: Group, board: Board) -> BoardAccessPermissio @staticmethod def permission_list_by_user( - user: UserProfile, board: Board + user: settings.AUTH_USER_MODEL, board: Board ) -> BoardAccessPermission: - groups = user.groups + groups = UserGroup.search_by_user(user) permissions = BoardAccessPermission(user, board) for group in groups: groupPerms = BoardPermission.objects.filter(group=group, board=board) @@ -153,7 +157,7 @@ def add_permission_bulk_by_board( perms: list[tuple[int, BoardAccessPermissionType]], ): for group_id, perm in perms: - BoardPermission.objects.create( + BoardPermission.objects.get_or_create( group=Group.search_by_id(group_id), board=board, permission=perm, diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index c5a8fe34..1f268150 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -123,6 +123,10 @@ def __str__(self) -> str: def can_change_nickname(self) -> bool: return (timezone.now() - relativedelta(months=3)) >= self.nickname_updated_at + @staticmethod + def get_by_auth_user(user: settings.AUTH_USER_MODEL) -> "UserProfile": + return UserProfile.objects.get(user=user) + @cached_property def email(self) -> str: return self.user.email @@ -141,7 +145,7 @@ def realname(self) -> str: def is_official(self) -> bool: if self.user.email == "new-ara@sparcs.org": return True - for group in self.groups: + for group in self.groups(): if group.is_official: return True return False @@ -150,18 +154,17 @@ def is_official(self) -> bool: def is_school_admin(self) -> bool: return self.has_group_by_id(7) # 7 = Communication board admin - @cached_property def groups(self) -> list[Group]: return UserGroup.search_by_user(self.user) def has_group(self, group: Group) -> bool: - return group in self.groups + return group in self.groups() def has_group_by_name(self, name: str) -> bool: - return any([group.name == name for group in self.groups]) + return any([group.name == name for group in self.groups()]) def has_group_by_id(self, group_id: int) -> bool: - return any([group.group_id == group_id for group in self.groups]) + return any([group.group_id == group_id for group in self.groups()]) def add_group(self, group: Group) -> None: if not self.has_group(group): diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py index b60d8851..2d7aed64 100644 --- a/apps/user/models/usergroup.py +++ b/apps/user/models/usergroup.py @@ -30,17 +30,15 @@ class Meta: ) @staticmethod - def search_by_user(self, user: UserProfile) -> list[Group]: + def search_by_user(user: settings.AUTH_USER_MODEL) -> list[Group]: groups = [] - for usergroup in UserGroup.objects.filter(user=user): + for usergroup in UserGroup.objects.filter(user=user).all(): groups.append(usergroup.group) return groups @staticmethod - def search_by_group( - self, group: Group - ) -> list[UserProfile]: # WARNING: Too many results + def search_by_group(group: Group) -> list[UserProfile]: # WARNING: Too many results users = [] - for usergroup in UserGroup.objects.filter(group=group): + for usergroup in UserGroup.objects.filter(group=group).all(): users.append(usergroup.user) return users From 234f02364e1187454af04335a24a2a798ce37b0c Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Thu, 11 Jan 2024 06:51:39 +0000 Subject: [PATCH 21/36] fix(permission): Add all board permissions --- tests/conftest.py | 46 +++++++++-------------------- tests/test_articles.py | 12 ++++++++ tests/test_block.py | 2 ++ tests/test_comments.py | 2 ++ tests/test_communication_article.py | 2 ++ tests/test_recent.py | 1 + tests/test_report.py | 1 + tests/test_scrap.py | 1 + tests/test_user.py | 2 ++ 9 files changed, 37 insertions(+), 32 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 33c62e31..7404b650 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,7 +47,7 @@ def set_user_client(request): "last_name": "LastName", }, ) - UserGroup.objects.create( + UserGroup.objects.get_or_create( user=request.cls.user, group=Group.search_by_id(2), # 2 = KAIST_MEMBER ) @@ -71,7 +71,7 @@ def set_user_client2(request): "last_name": "LastName", }, ) - UserGroup.objects.create( + UserGroup.objects.get_or_create( user=request.cls.user2, group=Group.search_by_id(2), # 2 = KAIST_MEMBER ) @@ -94,7 +94,7 @@ def set_user_client3(request): "last_name": "LastName", }, ) - UserGroup.objects.create( + UserGroup.objects.get_or_create( user=request.cls.user3, group=Group.search_by_id(2), # 2 = KAIST_MEMBER ) @@ -118,7 +118,7 @@ def set_user_client4(request): "last_name": "LastName", }, ) - UserGroup.objects.create( + UserGroup.objects.get_or_create( user=request.cls.user4, group=Group.search_by_id(2), # 2 = KAIST_MEMBER ) @@ -138,7 +138,7 @@ def set_user_with_kaist_info(request): sso_user_info={"kaist_info": '{"ku_kname": "user_with_kaist_info"}'}, agree_terms_of_service_at=timezone.now(), ) - UserGroup.objects.create( + UserGroup.objects.get_or_create( user=request.cls.user_with_kaist_info, group=Group.search_by_id(2), # 2 = KAIST_MEMBER ) @@ -162,7 +162,7 @@ def set_user_without_kaist_info(request): }, agree_terms_of_service_at=timezone.now(), ) - UserGroup.objects.create( + UserGroup.objects.get_or_create( user=request.cls.user_without_kaist_info, group=Group.search_by_id(2), # 2 = KAIST_MEMBER ) @@ -186,7 +186,7 @@ def set_school_admin(request): "last_name": "LastName", }, ) - UserGroup.objects.create( + UserGroup.objects.get_or_create( user=request.cls.school_admin, group=Group.search_by_id(7), # 7 = Communication board admin ) @@ -226,8 +226,10 @@ def create_user( username: str = "User", email: str = "user@sparcs.org", nickname: str = "Nickname", - group: Group = Group.search_by_id(2), # 2 = KAIST_MEMBER + group: int = 2 + # group: Group = Group.search_by_id(2), # 2 = KAIST_MEMBER ) -> User: + group = Group.search_by_id(2) user, created = User.objects.get_or_create(username=username, email=email) if created: UserProfile.objects.create( @@ -240,7 +242,7 @@ def create_user( "last_name": f"Lastname", }, ) - UserGroup.objects.create(user=user, group=group) + UserGroup.objects.get_or_create(user=user, group=group) return user @classmethod @@ -250,34 +252,14 @@ def create_user_with_index(cls, idx: int, group: Group) -> User: email=f"user{idx}@sparcs.org", nickname=f"Nickname{idx}", ) - UserGroup.objects.create(user=user, group=group) + UserGroup.objects.get_or_create(user=user, group=group) return user @classmethod def create_users( cls, num: int, - group: Group = Group.search_by_id(2), # 2 = KAIST_MEMBER + group: int = 2, # 2 = KAIST_MEMBER ) -> list[User]: + group = Group.search_by_id(2) return [cls.create_user_with_index(idx, group) for idx in range(num)] - - @classmethod - def add_default_groups(cls): - default_groups = { - 1: ("Unauthorized user", "뉴아라 계정을 만들지 않은 사람들", False), - 2: ("KAIST member", "카이스트 메일을 가진 사람 (학생, 교직원)", False), - 3: ("Store employee", "교내 입주 업체 직원", False), - 4: ("Other member", "카이스트 메일이 없는 개인 (특수한 관련자 등)", False), - 5: ("KAIST organization", "교내 학생 단체들", True), - 6: ("External organization", "외부인 (홍보 계정 등)", True), - 7: ("Communication board admin", "소통게시판 관리인", False), - 8: ("News board admin", "뉴스게시판 관리인", False), - } - - for group_id, (name, description, is_official) in default_groups.items(): - Group.objects.create( - group_id=group_id, - name=name, - description=description, - is_official=is_official, - ) diff --git a/tests/test_articles.py b/tests/test_articles.py index 5c9978d2..f9621b4d 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -7,6 +7,7 @@ from apps.core.models.board import NameType from apps.core.models.board_permission import ( DEFAULT_COMMENT_PERMISSION, + DEFAULT_PERMISSIONS, DEFAULT_READ_PERMISSION, DEFAULT_WRITE_PERMISSION, BoardAccessPermissionType, @@ -25,6 +26,7 @@ def set_boards(request): en_name="Test Board", name_type=NameType.REGULAR, ) + request.cls.board.set_default_permission() request.cls.anon_board = Board.objects.create( slug="anonymous", @@ -32,6 +34,7 @@ def set_boards(request): en_name="Anonymous", name_type=NameType.ANONYMOUS, ) + request.cls.anon_board.set_default_permission() request.cls.free_board = Board.objects.create( slug="free", @@ -39,6 +42,7 @@ def set_boards(request): en_name="Free", name_type=NameType.ANONYMOUS | NameType.REGULAR, ) + request.cls.free_board.set_default_permission() request.cls.realname_board = Board.objects.create( slug="test realname board", @@ -46,6 +50,7 @@ def set_boards(request): en_name="Test realname Board", name_type=NameType.REALNAME, ) + request.cls.realname_board.set_default_permission() request.cls.regular_access_board = Board.objects.create( slug="regular access", @@ -775,6 +780,12 @@ def test_being_topped(self): """ THRESHOLD = 5 board = Board.objects.create(top_threshold=THRESHOLD) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [] + permission_bulk.extend(DEFAULT_READ_PERMISSION) + permission_bulk.extend(DEFAULT_WRITE_PERMISSION) + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board(board, permission_bulk) + article = Article.objects.create(created_by=self.user, parent_board=board) pk = article.pk @@ -782,6 +793,7 @@ def test_being_topped(self): *users_ex_one, last_user = users for user in users_ex_one: + print(BoardPermission.permission_list_by_user(user, board).READ) self.http_request(user, "post", f"articles/{pk}/vote_positive") article = Article.objects.get(pk=pk) diff --git a/tests/test_block.py b/tests/test_block.py index 6684e870..835f3b61 100644 --- a/tests/test_block.py +++ b/tests/test_block.py @@ -16,6 +16,7 @@ def set_board(request): ko_name="테스트 게시판", en_name="Test Board", ) + request.cls.board.set_default_permission() @pytest.fixture(scope="class") @@ -26,6 +27,7 @@ def set_anon_board(request): en_name="Anonymous", name_type=NameType.ANONYMOUS, ) + request.cls.anon_board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_comments.py b/tests/test_comments.py index 3daf6bca..3ae616c9 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -20,6 +20,7 @@ def set_boards(request): en_name="Test Board", name_type=NameType.REGULAR, ) + request.cls.board.set_default_permission() request.cls.realname_board = Board.objects.create( slug="test realname board", @@ -27,6 +28,7 @@ def set_boards(request): en_name="Test realname Board", name_type=NameType.REALNAME, ) + request.cls.realname_board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index e8b5ebd1..5cb194a6 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -10,6 +10,7 @@ from apps.core.models.board import NameType from apps.core.models.board_permission import ( DEFAULT_COMMENT_PERMISSION, + DEFAULT_PERMISSIONS, DEFAULT_READ_PERMISSION, DEFAULT_WRITE_PERMISSION, BoardAccessPermissionType, @@ -63,6 +64,7 @@ def set_non_communication_board(request): en_name="Not With School (Test)", is_school_communication=False, ) + request.cls.non_communication_board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_recent.py b/tests/test_recent.py index 060ec588..a3044b28 100644 --- a/tests/test_recent.py +++ b/tests/test_recent.py @@ -17,6 +17,7 @@ def set_board(request): ko_name="테스트 게시판", en_name="Test Board", ) + request.cls.board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_report.py b/tests/test_report.py index 3ff04750..5c1da8be 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -14,6 +14,7 @@ def set_board(request): ko_name="테스트 게시판", en_name="Test Board", ) + request.cls.board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_scrap.py b/tests/test_scrap.py index 32146634..29946ac0 100644 --- a/tests/test_scrap.py +++ b/tests/test_scrap.py @@ -12,6 +12,7 @@ def set_board(request): ko_name="자유 게시판", en_name="Free Board", ) + request.cls.board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_user.py b/tests/test_user.py index eddae31b..1a81b3a7 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -18,6 +18,7 @@ def set_boards(request): ko_name="테스트 게시판", en_name="Test Board", ) + request.cls.board.set_default_permission() request.cls.realname_board = Board.objects.create( slug="realname", @@ -25,6 +26,7 @@ def set_boards(request): en_name="Realname Board", name_type=NameType.REALNAME, ) + request.cls.realname_board.set_default_permission() @pytest.fixture(scope="class") From 2bea42606ba2c570d939ab5cd92e8bf64b17d5a0 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Thu, 11 Jan 2024 09:19:27 +0000 Subject: [PATCH 22/36] refactor(permission): remove unnecessery prints --- apps/core/models/board.py | 2 +- tests/test_articles.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 65fee4b9..38386c2d 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -109,7 +109,7 @@ def __str__(self) -> str: def permission_list_by_group(self, group: Group) -> BoardAccessPermission: return BoardPermission.permission_list_by_group(group, self) - def permission_list_by_user(self, user: UserProfile) -> BoardAccessPermission: + def permission_list_by_user(self, user) -> BoardAccessPermission: return BoardPermission.permission_list_by_user(user, self) def set_default_permission(self): diff --git a/tests/test_articles.py b/tests/test_articles.py index f9621b4d..1d848e9a 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -793,7 +793,6 @@ def test_being_topped(self): *users_ex_one, last_user = users for user in users_ex_one: - print(BoardPermission.permission_list_by_user(user, board).READ) self.http_request(user, "post", f"articles/{pk}/vote_positive") article = Article.objects.get(pk=pk) From e4ad24a28a75a299654f6ec38acdf04031e039f4 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Thu, 11 Jan 2024 09:57:35 +0000 Subject: [PATCH 23/36] refactor(permission): Remove Old Structures --- apps/core/models/board.py | 16 ---------------- apps/user/models/user_profile.py | 20 -------------------- 2 files changed, 36 deletions(-) diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 38386c2d..3a37c1d1 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -114,19 +114,3 @@ def permission_list_by_user(self, user) -> BoardAccessPermission: def set_default_permission(self): BoardPermission.add_permission_bulk_by_board(self, DEFAULT_PERMISSIONS) - - # def old_group_has_access_permission( - # self, access_type: OldBoardAccessPermissionType, group: int - # ) -> bool: - # mask = None - # if access_type == OldBoardAccessPermissionType.READ: - # mask = self.read_access_mask - # elif access_type == OldBoardAccessPermissionType.WRITE: - # mask = self.write_access_mask - # elif access_type == OldBoardAccessPermissionType.COMMENT: - # mask = self.comment_access_mask - # else: - # # TODO: Handle error - # return False - # - # return (mask & (1 << group)) > 0 diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index 1f268150..c3178663 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -24,26 +24,6 @@ class Meta(MetaDataModel.Meta): ("nickname", "is_newara", "deleted_at"), ) - class OldUserGroup(models.IntegerChoices): - # 뉴아라 계정을 만들지 않은 사람들 - UNAUTHORIZED = 0, gettext_lazy("Unauthorized user") - # 카이스트 메일을 가진 사람 (학생, 교직원) - KAIST_MEMBER = 1, gettext_lazy("KAIST member") - # 교내 입주 업체 직원 - STORE_EMPLOYEE = 2, gettext_lazy("Store employee") - # 카이스트 메일이 없는 개인 (특수한 관련자 등) - OTHER_MEMBER = 3, gettext_lazy("Other member") - # 교내 학생 단체들 - KAIST_ORG = 4, gettext_lazy("KAIST organization") - # 외부인 (홍보 계정 등) - EXTERNAL_ORG = 5, gettext_lazy("External organization") - # 소통게시판 관리인 - COMMUNICATION_BOARD_ADMIN = 6, gettext_lazy("Communication board admin") - # 뉴스게시판 관리인 - NEWS_BOARD_ADMIN = 7, gettext_lazy("News board admin") - - OLD_OFFICIAL_GROUPS = [OldUserGroup.STORE_EMPLOYEE, OldUserGroup.KAIST_ORG] - uid = models.CharField( null=True, default=None, From 0d53fe85780f4f70d8ac09b1bad1ad50ba79a5c5 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Thu, 11 Jan 2024 10:04:31 +0000 Subject: [PATCH 24/36] refactor(permission): move meta to down --- apps/core/models/board_group.py | 8 ++++---- apps/core/models/board_permission.py | 10 +++++----- apps/user/models/group.py | 8 ++++---- apps/user/models/usergroup.py | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/core/models/board_group.py b/apps/core/models/board_group.py index b2d6804d..3f78fc04 100644 --- a/apps/core/models/board_group.py +++ b/apps/core/models/board_group.py @@ -2,10 +2,6 @@ class BoardGroup(models.Model): - class Meta: - verbose_name = "게시판 그룹" - verbose_name_plural = "게시판 그룹 목록" - ko_name = models.CharField( verbose_name="게시판 그룹 국문 이름", max_length=64, @@ -20,5 +16,9 @@ class Meta: unique=True, ) + class Meta: + verbose_name = "게시판 그룹" + verbose_name_plural = "게시판 그룹 목록" + def __str__(self) -> str: return self.ko_name diff --git a/apps/core/models/board_permission.py b/apps/core/models/board_permission.py index 797b75ca..4f1e2868 100644 --- a/apps/core/models/board_permission.py +++ b/apps/core/models/board_permission.py @@ -83,11 +83,6 @@ def setPermission(self, permission: BoardAccessPermissionType) -> None: class BoardPermission(models.Model): - class Meta: - verbose_name = "BoardPermission" - verbose_name_plural = "BoardPermissions" - unique_together = (("group", "board", "permission"),) - group = models.ForeignKey( on_delete=models.CASCADE, to="user.Group", @@ -105,6 +100,11 @@ class Meta: null=False, ) + class Meta: + verbose_name = "BoardPermission" + verbose_name_plural = "BoardPermissions" + unique_together = (("group", "board", "permission"),) + @staticmethod def permission_list_by_group(group: Group, board: Board) -> BoardAccessPermission: permissions = BoardAccessPermission(group, board) diff --git a/apps/user/models/group.py b/apps/user/models/group.py index 888eb56b..9a8aa6fe 100644 --- a/apps/user/models/group.py +++ b/apps/user/models/group.py @@ -3,10 +3,6 @@ class Group(models.Model): - class Meta: - verbose_name = "Group" - verbose_name_plural = "Groups" - group_id = models.AutoField( verbose_name="Group ID", primary_key=True, @@ -28,6 +24,10 @@ class Meta: default=False, ) + class Meta: + verbose_name = "Group" + verbose_name_plural = "Groups" + def __str__(self) -> str: return self.name diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py index 2d7aed64..6551d506 100644 --- a/apps/user/models/usergroup.py +++ b/apps/user/models/usergroup.py @@ -10,11 +10,6 @@ class UserGroup(models.Model): - class Meta: - verbose_name = "사용자 그룹" - verbose_name_plural = "사용자가 속한 그룹 목록" - unique_together = (("user", "group"),) - user = models.ForeignKey( verbose_name="사용자", on_delete=models.CASCADE, @@ -29,6 +24,11 @@ class Meta: db_index=True, ) + class Meta: + verbose_name = "사용자 그룹" + verbose_name_plural = "사용자가 속한 그룹 목록" + unique_together = (("user", "group"),) + @staticmethod def search_by_user(user: settings.AUTH_USER_MODEL) -> list[Group]: groups = [] From 2e80713b70e0fcd3e1a15ab20fe13a3f8dd80b33 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Thu, 11 Jan 2024 10:04:44 +0000 Subject: [PATCH 25/36] refactor(permission): Remove Old Structures --- apps/core/models/board.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 3a37c1d1..c3f57af6 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -24,12 +24,6 @@ class NameType(IntFlag): REALNAME = auto() -class OldBoardAccessPermissionType(IntEnum): - READ = 0 - WRITE = 1 - COMMENT = 2 - - class Board(MetaDataModel): slug = AutoSlugField( populate_from=["en_name"], From 193a91274a91c8b2d2e051520f6c5b416d01474e Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Thu, 11 Jan 2024 10:07:20 +0000 Subject: [PATCH 26/36] fix(permission): Restore manualuser org_type --- apps/user/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/user/admin.py b/apps/user/admin.py index b0673cf6..5e6a9ebb 100644 --- a/apps/user/admin.py +++ b/apps/user/admin.py @@ -31,6 +31,7 @@ class ManualUserAdmin(MetaDataModelAdmin): list_display = ( "user", "org_name", + "org_type", "applicant_name", "sso_email", ) From 4df23e8be1163cf75bc4ee0200bc8f60868ee70d Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 16 Jan 2024 13:55:34 +0000 Subject: [PATCH 27/36] refactor(permission): Remove commit in migration --- .../migrations/0022_group_remove_userprofile_group_and_more.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py b/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py index 7ebaecdf..0ef602ff 100644 --- a/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py +++ b/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py @@ -36,7 +36,6 @@ def add_user_groups(apps, schema_editor): WHERE (a.group+1) = b.group_id; """ ) - # schema_editor.connection.commit() cursor.close() From be94d66f62da4d1142e2e47ee8de6190d5b14cb0 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 16 Jan 2024 13:59:35 +0000 Subject: [PATCH 28/36] fix(permission): Revert org_type null=False --- .../migrations/0022_group_remove_userprofile_group_and_more.py | 2 -- apps/user/models/user/manual.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py b/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py index 0ef602ff..54fd2f85 100644 --- a/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py +++ b/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py @@ -132,8 +132,6 @@ class Migration(migrations.Migration): model_name="manualuser", name="org_type", field=models.ForeignKey( - blank=True, - null=True, on_delete=django.db.models.deletion.CASCADE, to="user.group", verbose_name="업체/단체 그룹", diff --git a/apps/user/models/user/manual.py b/apps/user/models/user/manual.py index 819e8d5f..bd6ce65d 100644 --- a/apps/user/models/user/manual.py +++ b/apps/user/models/user/manual.py @@ -29,8 +29,6 @@ class Meta(MetaDataModel.Meta): on_delete=models.CASCADE, to="user.Group", verbose_name="업체/단체 그룹", - null=True, - blank=True, ) applicant_name = models.CharField( From 529c709bcfc0de9094866cd8472eee19b1009e23 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 16 Jan 2024 14:11:51 +0000 Subject: [PATCH 29/36] fix(permission): Check is_school_admin not has_group_by_id --- apps/core/models/signals/comment.py | 4 +--- apps/core/permissions/communication_article.py | 4 +--- apps/core/views/viewsets/comment.py | 6 ++---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/core/models/signals/comment.py b/apps/core/models/signals/comment.py index 5cbfe4d2..3de7285b 100644 --- a/apps/core/models/signals/comment.py +++ b/apps/core/models/signals/comment.py @@ -33,9 +33,7 @@ def update_communication_article_status(comment): ) if ( article.parent_board.is_school_communication - and comment.created_by.profile.has_group_by_id( - 7 - ) # 7 = Communication board admin + and comment.created_by.profile.is_school_admin and article.communication_article.school_response_status != SchoolResponseStatus.ANSWER_DONE ): diff --git a/apps/core/permissions/communication_article.py b/apps/core/permissions/communication_article.py index 07db0477..98237df7 100644 --- a/apps/core/permissions/communication_article.py +++ b/apps/core/permissions/communication_article.py @@ -7,6 +7,4 @@ class CommunicationArticleAdminPermission(permissions.IsAuthenticated): message = "You are not authorized to access this feature" def has_permission(self, request, view): - return request.user.is_staff or request.user.profile.has_group_by_id( - 7 - ) # 7 = Communication board admin + return request.user.is_staff or request.user.profile.is_school_admin diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 4fdba5bf..1eb2f57f 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -84,12 +84,10 @@ def perform_create(self, serializer): ) parent_article = parent_comment.parent_article - print(parent_article) + # print(parent_article) created_by = self.request.user - is_school_admin = UserProfile.objects.get(user_id=created_by).has_group_by_id( - 7 - ) # 7 = Communication board admin + is_school_admin = UserProfile.objects.get(user_id=created_by).is_school_admin if is_school_admin and parent_article.name_type != NameType.ANONYMOUS: name_type = NameType.REGULAR From c1a6d8020abe370423119bc839f1d21bb9645480 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 16 Jan 2024 14:13:38 +0000 Subject: [PATCH 30/36] fix(permission): groups cached property, argument names, any() function --- apps/user/models/user_profile.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index c3178663..f86fc567 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -125,33 +125,30 @@ def realname(self) -> str: def is_official(self) -> bool: if self.user.email == "new-ara@sparcs.org": return True - for group in self.groups(): - if group.is_official: - return True - return False + return any(group.is_official for group in self.groups) @cached_property def is_school_admin(self) -> bool: return self.has_group_by_id(7) # 7 = Communication board admin + @cached_property def groups(self) -> list[Group]: return UserGroup.search_by_user(self.user) def has_group(self, group: Group) -> bool: - return group in self.groups() + return group in self.groups - def has_group_by_name(self, name: str) -> bool: - return any([group.name == name for group in self.groups()]) + def has_group_by_name(self, group_name: str) -> bool: + return any(group.name == group_name for group in self.groups) def has_group_by_id(self, group_id: int) -> bool: - return any([group.group_id == group_id for group in self.groups()]) + return any(group.group_id == group_id for group in self.groups) def add_group(self, group: Group) -> None: - if not self.has_group(group): - UserGroup.objects.create(user=self.user, group=group) + UserGroup.objects.get_or_create(user=self.user, group=group) - def add_group_by_name(self, name: str) -> None: - group = Group.search_by_name(name) + def add_group_by_name(self, group_name: str) -> None: + group = Group.search_by_name(group_name) self.add_group(group) def add_group_by_id(self, group_id: int) -> None: @@ -162,8 +159,8 @@ def remove_group(self, group: Group) -> None: if self.has_group(group): UserGroup.objects.get(user=self.user, group=group).delete() - def remove_group_by_name(self, name: str) -> None: - group = Group.search_by_name(name) + def remove_group_by_name(self, group_name: str) -> None: + group = Group.search_by_name(group_name) self.remove_group(group) def remove_group_by_id(self, group_id: int) -> None: From 1f43ed0ac060181622baa6bb4947986fb6d1cd2b Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 16 Jan 2024 14:16:49 +0000 Subject: [PATCH 31/36] fix(permission): Fix default official groups --- .../0022_group_remove_userprofile_group_and_more.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py b/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py index 54fd2f85..8b865e9d 100644 --- a/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py +++ b/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py @@ -9,10 +9,10 @@ def add_groups(apps, schema_editor): default_groups = { 1: ("Unauthorized user", "뉴아라 계정을 만들지 않은 사람들", False), 2: ("KAIST member", "카이스트 메일을 가진 사람 (학생, 교직원)", False), - 3: ("Store employee", "교내 입주 업체 직원", False), + 3: ("Store employee", "교내 입주 업체 직원", True), 4: ("Other member", "카이스트 메일이 없는 개인 (특수한 관련자 등)", False), 5: ("KAIST organization", "교내 학생 단체들", True), - 6: ("External organization", "외부인 (홍보 계정 등)", True), + 6: ("External organization", "외부인 (홍보 계정 등)", False), 7: ("Communication board admin", "소통게시판 관리인", False), 8: ("News board admin", "뉴스게시판 관리인", False), } From 283468e9eab3c56423dcd0080fe701aee6e9e4ff Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 16 Jan 2024 14:35:56 +0000 Subject: [PATCH 32/36] fix(permission): Remove by_name functions --- apps/user/models/user_profile.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index f86fc567..bbbe2fc5 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -147,10 +147,6 @@ def has_group_by_id(self, group_id: int) -> bool: def add_group(self, group: Group) -> None: UserGroup.objects.get_or_create(user=self.user, group=group) - def add_group_by_name(self, group_name: str) -> None: - group = Group.search_by_name(group_name) - self.add_group(group) - def add_group_by_id(self, group_id: int) -> None: group = Group.search_by_id(group_id) self.add_group(group) @@ -159,10 +155,6 @@ def remove_group(self, group: Group) -> None: if self.has_group(group): UserGroup.objects.get(user=self.user, group=group).delete() - def remove_group_by_name(self, group_name: str) -> None: - group = Group.search_by_name(group_name) - self.remove_group(group) - def remove_group_by_id(self, group_id: int) -> None: group = Group.search_by_id(group_id) self.remove_group(group) From 32ef4496abdd5440b31b9371aef69b9ef4ff3431 Mon Sep 17 00:00:00 2001 From: DoyunShin Date: Tue, 16 Jan 2024 14:36:09 +0000 Subject: [PATCH 33/36] fix(permission): Use Group join --- apps/user/models/usergroup.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py index 6551d506..c0f12107 100644 --- a/apps/user/models/usergroup.py +++ b/apps/user/models/usergroup.py @@ -5,8 +5,10 @@ from django.conf import settings from django.db import models +from .group import Group + if TYPE_CHECKING: - from apps.user.models import Group, UserProfile + from apps.user.models import UserProfile class UserGroup(models.Model): @@ -31,14 +33,8 @@ class Meta: @staticmethod def search_by_user(user: settings.AUTH_USER_MODEL) -> list[Group]: - groups = [] - for usergroup in UserGroup.objects.filter(user=user).all(): - groups.append(usergroup.group) - return groups + return Group.objects.filter(usergroup__user=user).all() @staticmethod def search_by_group(group: Group) -> list[UserProfile]: # WARNING: Too many results - users = [] - for usergroup in UserGroup.objects.filter(group=group).all(): - users.append(usergroup.user) - return users + return UserProfile.objects.filter(usergroup__group=group).all() From 3e12f68b875aed5a160252dd0edf409fdee6b0eb Mon Sep 17 00:00:00 2001 From: yuwol Date: Tue, 23 Jan 2024 14:17:45 +0000 Subject: [PATCH 34/36] refactor(permission): use annotations for self typing --- apps/user/models/group.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/user/models/group.py b/apps/user/models/group.py index 9a8aa6fe..53e8fc44 100644 --- a/apps/user/models/group.py +++ b/apps/user/models/group.py @@ -1,4 +1,5 @@ -from django.conf import settings +from __future__ import annotations + from django.db import models @@ -32,9 +33,9 @@ def __str__(self) -> str: return self.name @staticmethod - def search_by_name(name: str) -> list["Group"]: + def search_by_name(name: str) -> list[Group]: return Group.objects.filter(name=name) @staticmethod - def search_by_id(group_id: int) -> "Group": + def search_by_id(group_id: int) -> Group: return Group.objects.get(group_id=group_id) From c71e9f2d8e4d911a97a1a00f0b89939280d0d9ef Mon Sep 17 00:00:00 2001 From: yuwol Date: Tue, 23 Jan 2024 14:29:24 +0000 Subject: [PATCH 35/36] chore(permission): remove unused imports --- apps/core/models/board.py | 8 ++------ apps/core/models/board_permission.py | 11 +++++------ apps/core/models/signals/comment.py | 1 - apps/core/permissions/communication_article.py | 2 -- apps/user/models/user/manual.py | 2 +- apps/user/models/user_profile.py | 1 - tests/test_articles.py | 1 - tests/test_communication_article.py | 3 --- 8 files changed, 8 insertions(+), 21 deletions(-) diff --git a/apps/core/models/board.py b/apps/core/models/board.py index c3f57af6..cc6c7009 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -1,19 +1,15 @@ -from enum import IntEnum, IntFlag, auto +from enum import IntFlag, auto from django.db import models from django_extensions.db.fields import AutoSlugField -from apps.user.models import Group, UserProfile +from apps.user.models import Group from ara.db.models import MetaDataModel from .board_group import BoardGroup from .board_permission import ( - DEFAULT_COMMENT_PERMISSION, DEFAULT_PERMISSIONS, - DEFAULT_READ_PERMISSION, - DEFAULT_WRITE_PERMISSION, BoardAccessPermission, - BoardAccessPermissionType, BoardPermission, ) diff --git a/apps/core/models/board_permission.py b/apps/core/models/board_permission.py index 4f1e2868..7cde03af 100644 --- a/apps/core/models/board_permission.py +++ b/apps/core/models/board_permission.py @@ -1,12 +1,11 @@ from __future__ import annotations from enum import IntEnum -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from django.conf import settings from django.db import models -import apps.core.models.board as board from apps.user.models import Group, UserGroup, UserProfile if TYPE_CHECKING: @@ -84,16 +83,16 @@ def setPermission(self, permission: BoardAccessPermissionType) -> None: class BoardPermission(models.Model): group = models.ForeignKey( - on_delete=models.CASCADE, + verbose_name="group", to="user.Group", + on_delete=models.CASCADE, db_index=True, - verbose_name="group", ) board = models.ForeignKey( - on_delete=models.CASCADE, + verbose_name="board slug", to="core.Board", + on_delete=models.CASCADE, db_index=True, - verbose_name="board slug", ) permission: int = models.SmallIntegerField( verbose_name="permission", diff --git a/apps/core/models/signals/comment.py b/apps/core/models/signals/comment.py index 3de7285b..e2ecdcb8 100644 --- a/apps/core/models/signals/comment.py +++ b/apps/core/models/signals/comment.py @@ -4,7 +4,6 @@ from apps.core.models import Comment, Notification from apps.core.models.communication_article import SchoolResponseStatus -from apps.user.models import UserProfile @receiver(models.signals.post_save, sender=Comment) diff --git a/apps/core/permissions/communication_article.py b/apps/core/permissions/communication_article.py index 98237df7..02862bb3 100644 --- a/apps/core/permissions/communication_article.py +++ b/apps/core/permissions/communication_article.py @@ -1,7 +1,5 @@ from rest_framework import permissions -from apps.user.models import UserProfile - class CommunicationArticleAdminPermission(permissions.IsAuthenticated): message = "You are not authorized to access this feature" diff --git a/apps/user/models/user/manual.py b/apps/user/models/user/manual.py index bd6ce65d..0f930a6a 100644 --- a/apps/user/models/user/manual.py +++ b/apps/user/models/user/manual.py @@ -1,7 +1,7 @@ from django.conf import settings from django.db import models -from apps.user.models import Group, UserProfile +from apps.user.models import Group from ara.db.models import MetaDataModel diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index bbbe2fc5..3ea77b8d 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -5,7 +5,6 @@ from django.db import models from django.utils import timezone from django.utils.functional import cached_property -from django.utils.translation import gettext_lazy from apps.user.models import Group from ara.db.models import MetaDataModel diff --git a/tests/test_articles.py b/tests/test_articles.py index 1d848e9a..28c8ebd9 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -7,7 +7,6 @@ from apps.core.models.board import NameType from apps.core.models.board_permission import ( DEFAULT_COMMENT_PERMISSION, - DEFAULT_PERMISSIONS, DEFAULT_READ_PERMISSION, DEFAULT_WRITE_PERMISSION, BoardAccessPermissionType, diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index 5cb194a6..a4e1ec48 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -10,9 +10,6 @@ from apps.core.models.board import NameType from apps.core.models.board_permission import ( DEFAULT_COMMENT_PERMISSION, - DEFAULT_PERMISSIONS, - DEFAULT_READ_PERMISSION, - DEFAULT_WRITE_PERMISSION, BoardAccessPermissionType, BoardPermission, ) From 3a22da3dc3c74399e41d9488420de69526c17eed Mon Sep 17 00:00:00 2001 From: yuwol Date: Tue, 23 Jan 2024 17:42:30 +0000 Subject: [PATCH 36/36] fix(permission): fix admin page for `UserGroup` --- apps/user/admin.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/user/admin.py b/apps/user/admin.py index 5e6a9ebb..0ea863cd 100644 --- a/apps/user/admin.py +++ b/apps/user/admin.py @@ -38,25 +38,34 @@ class ManualUserAdmin(MetaDataModelAdmin): @admin.register(UserGroup) -class UserGroupAdmin(MetaDataModelAdmin): +class UserGroupAdmin(admin.ModelAdmin): list_display = ( - "user_id", - "group_id", + "user", + "nickname", + "email", + "group", ) + list_filter = ("group",) search_fields = ( - "user_id", - "group_id", + "user__id", + "user__profile__nickname", + "user__email", ) + @admin.display(description="닉네임") + def nickname(self, obj: UserGroup): + return obj.user.profile.nickname + + @admin.display(description="이메일") + def email(self, obj: UserGroup): + return obj.user.email + @admin.register(Group) -class GroupAdmin(MetaDataModelAdmin): +class GroupAdmin(admin.ModelAdmin): list_display = ( - "group_id", "name", + "description", "is_official", ) - search_fields = ( - "id", - "name", - ) + search_fields = ("name",)