ssh key저장 기능 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 1m57s
All checks were successful
Build And Test / build-and-push (push) Successful in 1m57s
This commit is contained in:
@ -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
|
||||
|
@ -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
|
@ -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)
|
||||
|
18
users/migrations/0004_customuser_encrypted_private_key.py
Normal file
18
users/migrations/0004_customuser_encrypted_private_key.py
Normal 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),
|
||||
),
|
||||
]
|
18
users/migrations/0005_customuser_last_used_at.py
Normal file
18
users/migrations/0005_customuser_last_used_at.py
Normal 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 키 마지막 사용 시각'),
|
||||
),
|
||||
]
|
@ -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 키 이름'),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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"),
|
||||
]
|
||||
|
@ -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)
|
Reference in New Issue
Block a user