ssh key저장 기능 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 1m57s

This commit is contained in:
2025-05-20 13:25:12 +09:00
parent 9c84584c36
commit 293003cf1c
10 changed files with 149 additions and 15 deletions

View File

@ -20,6 +20,7 @@ RUN apt-get clean
# install python dependencies
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
RUN pip install gunicorn==20.1.0
# collect static files
# RUN python manage.py collectstatic --noinput

View File

@ -1,13 +1,16 @@
asgiref==3.8.1
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
coreapi==2.3.3
coreschema==0.0.4
cryptography==45.0.2
Django==4.2.14
django-cors-headers==4.7.0
djangorestframework==3.16.0
djangorestframework_simplejwt==5.5.0
drf-yasg==1.21.10
gunicorn==20.1.0
idna==3.10
inflection==0.5.1
itypes==1.2.0
@ -15,6 +18,7 @@ Jinja2==3.1.6
MarkupSafe==3.0.2
mysqlclient==2.2.7
packaging==25.0
pycparser==2.22
PyJWT==2.9.0
python-dotenv==1.0.1
pytz==2025.2
@ -24,4 +28,3 @@ sqlparse==0.5.3
typing_extensions==4.13.2
uritemplate==4.1.1
urllib3==2.4.0
gunicorn==20.1.0

View File

@ -6,26 +6,44 @@ from rest_framework_simplejwt.tokens import RefreshToken
class CustomUserAdmin(UserAdmin):
model = CustomUser
list_display = ('email', 'name', 'grade', 'desc', 'is_active', 'is_staff', 'jwt_preview')
list_display = (
'email', 'name', 'grade', 'desc',
'is_active', 'is_staff', 'jwt_preview',
'encrypted_private_key_name' # ✅ 키 이름 컬럼 추가
)
list_filter = ('grade', 'is_active', 'is_staff')
search_fields = ('email', 'name')
ordering = ('email',)
readonly_fields = ('created_at', 'last_login', 'jwt_preview') # ✅ jwt_preview 추가
readonly_fields = (
'created_at', 'last_login', 'jwt_preview',
'encrypted_private_key_name', # ✅ 키 이름 읽기 전용
'encrypted_private_key_preview'
)
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal Info', {'fields': ('name', 'grade', 'desc')}),
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
('Important dates', {'fields': ('last_login', 'created_at')}),
('JWT Token Preview', {'fields': ('jwt_preview',)}), # ✅ 별도 섹션으로 추가
('Permissions', {'fields': (
'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions'
)}),
('Important dates', {'fields': ('last_login',)}),
('JWT Token Preview', {'fields': ('jwt_preview',)}),
('SSH Key Info', { # ✅ 키 정보 그룹
'fields': (
'encrypted_private_key_name',
'encrypted_private_key_preview'
)
}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'email', 'name', 'grade', 'desc',
'email', 'name', 'grade', 'desc',
'password1', 'password2',
'is_active', 'is_staff', 'is_superuser'
),
@ -36,11 +54,16 @@ class CustomUserAdmin(UserAdmin):
try:
refresh = RefreshToken.for_user(obj)
access_token = str(refresh.access_token)
return access_token[:30] + "..." # ✅ 일부만 보여줌
return access_token[:30] + "..."
except Exception:
return "N/A"
jwt_preview.short_description = "JWT Access Preview"
def encrypted_private_key_preview(self, obj):
if obj.encrypted_private_key:
return str(obj.encrypted_private_key)[:30] + "..."
return "없음"
encrypted_private_key_preview.short_description = "SSH Key (암호화)"
admin.site.register(CustomUser, CustomUserAdmin)

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.14 on 2025-05-20 03:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0003_alter_customuser_is_active'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='encrypted_private_key',
field=models.BinaryField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.14 on 2025-05-20 03:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0004_customuser_encrypted_private_key'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='last_used_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='SSH 키 마지막 사용 시각'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.14 on 2025-05-20 04:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0005_customuser_last_used_at'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='encrypted_private_key_name',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='SSH 키 이름'),
),
]

View File

@ -1,5 +1,8 @@
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
import base64
from cryptography.fernet import Fernet
from django.utils import timezone
class CustomUserManager(BaseUserManager):
@ -15,7 +18,7 @@ class CustomUserManager(BaseUserManager):
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("grade", "admin") # 슈퍼유저는 기본 admin
extra_fields.setdefault("grade", "admin")
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
@ -30,7 +33,6 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
('admin', '관리자'),
('manager', '매니저'),
('user', '일반 사용자'),
)
email = models.EmailField(unique=True)
@ -38,10 +40,15 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
grade = models.CharField(max_length=20, choices=GRADE_CHOICES, default='user')
desc = models.TextField(blank=True, null=True, verbose_name="설명")
is_active = models.BooleanField(default=False) # 최초 가입 비활성
is_active = models.BooleanField(default=False)
is_staff = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
# 🔐 SSH 키 관련 필드
encrypted_private_key_name = models.CharField(max_length=100, blank=True, null=True, verbose_name="SSH 키 이름")
encrypted_private_key = models.BinaryField(blank=True, null=True)
last_used_at = models.DateTimeField(blank=True, null=True, verbose_name="SSH 키 마지막 사용 시각")
objects = CustomUserManager()
USERNAME_FIELD = 'email'
@ -49,3 +56,26 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
def __str__(self):
return self.email
# 🔐 SSH Private Key 암복호화 관련 메서드
def encrypt_private_key(self, private_key: str) -> bytes:
cipher = Fernet(self.get_encryption_key())
return cipher.encrypt(private_key.encode())
def decrypt_private_key(self) -> str:
if self.encrypted_private_key:
cipher = Fernet(self.get_encryption_key())
decrypted = cipher.decrypt(self.encrypted_private_key).decode()
self.last_used_at = timezone.now()
self.save(update_fields=['last_used_at']) # 📌 사용 시각 업데이트
return decrypted
return ""
def save_private_key(self, private_key: str):
self.encrypted_private_key = self.encrypt_private_key(private_key)
self.save()
def get_encryption_key(self) -> bytes:
email_encoded = self.email.encode()
base64_key = base64.urlsafe_b64encode(email_encoded.ljust(32)[:32])
return base64_key

View File

@ -1,5 +1,5 @@
from django.urls import path
from .views import RegisterView, MeView, CustomTokenObtainPairView
from .views import RegisterView, MeView, CustomTokenObtainPairView, SSHKeyUploadView
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView
urlpatterns = [
@ -9,4 +9,5 @@ urlpatterns = [
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('verify/', TokenVerifyView.as_view(), name='token_verify'),
path('me/', MeView.as_view(), name='me'),
path("ssh-key/", SSHKeyUploadView.as_view(), name="ssh_key_upload"),
]

View File

@ -31,4 +31,26 @@ class MeView(APIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
serializer_class = CustomTokenObtainPairSerializer
class SSHKeyUploadView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
private_key = request.data.get("private_key")
key_name = request.data.get("key_name") # 여전히 key_name으로 받음
if not private_key or not key_name:
return Response(
{"error": "private_key와 key_name 모두 필요합니다."},
status=status.HTTP_400_BAD_REQUEST
)
user = request.user
try:
user.save_private_key(private_key)
user.encrypted_private_key_name = key_name
user.save(update_fields=["encrypted_private_key", "encrypted_private_key_name"])
return Response({"message": "SSH key 저장 완료."})
except Exception as e:
return Response({"error": str(e)}, status=500)

View File

@ -1 +1 @@
0.0.8-r1
0.0.9