SECURITY - TASKER: cleanup access and invite approval
This commit is contained in:
@@ -70,7 +70,6 @@ def get_access_config():
|
||||
token = (
|
||||
os.environ.get("PLANE_NODEDC_ACCESS_TOKEN", "").strip()
|
||||
or os.environ.get("NODEDC_INTERNAL_ACCESS_TOKEN", "").strip()
|
||||
or os.environ.get("PLANE_OIDC_CLIENT_SECRET", "").strip()
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -185,4 +185,8 @@ def nodedc_launcher_managed_workspace_response():
|
||||
|
||||
|
||||
def is_nodedc_workspace_invite_approval_required(policy):
|
||||
return bool(policy.get("enabled")) and policy.get("managed_by") == "tasker" and policy.get("invite_approval") == "nodedc"
|
||||
return (
|
||||
bool(policy.get("enabled"))
|
||||
and policy.get("managed_by") == "tasker"
|
||||
and policy.get("invite_approval") in {"nodedc", "launcher"}
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from secrets import compare_digest
|
||||
from django.contrib.auth import logout
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -13,7 +14,7 @@ from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from plane.authentication.utils.host import user_ip
|
||||
from plane.db.models import ExternalIdentityLink, Session, User
|
||||
from plane.db.models import ExternalIdentityLink, IssueAssignee, ProjectMember, Session, User, WorkspaceMember
|
||||
|
||||
|
||||
OIDC_PROVIDER = "authentik"
|
||||
@@ -33,7 +34,6 @@ def get_nodedc_internal_token():
|
||||
return (
|
||||
os.environ.get("PLANE_NODEDC_ACCESS_TOKEN", "").strip()
|
||||
or os.environ.get("NODEDC_INTERNAL_ACCESS_TOKEN", "").strip()
|
||||
or os.environ.get("PLANE_OIDC_CLIENT_SECRET", "").strip()
|
||||
)
|
||||
|
||||
|
||||
@@ -225,6 +225,32 @@ def invalidate_user_sessions(user, request):
|
||||
return deleted_count
|
||||
|
||||
|
||||
def revoke_user_external_identity_links(user):
|
||||
deleted_count, _ = ExternalIdentityLink.objects.filter(
|
||||
provider=OIDC_PROVIDER,
|
||||
user=user,
|
||||
status=ExternalIdentityLink.Status.ACTIVE,
|
||||
).delete()
|
||||
return deleted_count
|
||||
|
||||
|
||||
def delete_queryset(queryset):
|
||||
result = queryset.delete()
|
||||
return result[0] if isinstance(result, tuple) else result
|
||||
|
||||
|
||||
def revoke_user_tasker_access(user):
|
||||
deleted_issue_assignees = delete_queryset(IssueAssignee.objects.filter(assignee=user))
|
||||
deleted_project_memberships = delete_queryset(ProjectMember.objects.filter(member=user))
|
||||
deleted_workspace_memberships = delete_queryset(WorkspaceMember.objects.filter(member=user))
|
||||
|
||||
return {
|
||||
"workspaceMemberships": deleted_workspace_memberships,
|
||||
"projectMemberships": deleted_project_memberships,
|
||||
"issueAssignees": deleted_issue_assignees,
|
||||
}
|
||||
|
||||
|
||||
class NodeDCFrontChannelLogoutEndpoint(View):
|
||||
def get(self, request):
|
||||
logout_current_user(request)
|
||||
@@ -269,12 +295,27 @@ class NodeDCInternalSessionLogoutEndpoint(View):
|
||||
return JsonResponse({"ok": True, "deletedSessions": 0, "user": None})
|
||||
|
||||
guard_keys = mark_logout_guard(user=user, payload=payload)
|
||||
deleted_sessions = invalidate_user_sessions(user, request)
|
||||
with transaction.atomic():
|
||||
deleted_sessions = invalidate_user_sessions(user, request)
|
||||
deleted_identity_links = 0
|
||||
deleted_tasker_access = {
|
||||
"workspaceMemberships": 0,
|
||||
"projectMemberships": 0,
|
||||
"issueAssignees": 0,
|
||||
}
|
||||
|
||||
if payload.get("revokeIdentityLinks") is True or payload.get("revokeIdentityLink") is True:
|
||||
deleted_identity_links = revoke_user_external_identity_links(user)
|
||||
|
||||
if payload.get("revokeTaskerAccess") is True or payload.get("revokeMemberships") is True:
|
||||
deleted_tasker_access = revoke_user_tasker_access(user)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
"deletedSessions": deleted_sessions,
|
||||
"deletedIdentityLinks": deleted_identity_links,
|
||||
"deletedTaskerAccess": deleted_tasker_access,
|
||||
"guardKeys": len(guard_keys),
|
||||
"user": {
|
||||
"id": str(user.id),
|
||||
|
||||
@@ -175,12 +175,28 @@ def resolve_nodedc_launcher_origin():
|
||||
return ""
|
||||
|
||||
|
||||
def sync_user_avatar_from_payload(user, payload):
|
||||
def sync_user_profile_from_payload(user, payload):
|
||||
updated_fields = []
|
||||
display_name = first_payload_string(payload, "displayName", "display_name", "name")
|
||||
avatar_url = normalize_nodedc_avatar_url(first_payload_string(payload, "avatarUrl", "avatar_url", "avatar"))
|
||||
|
||||
if display_name and user.display_name != display_name:
|
||||
user.display_name = display_name
|
||||
updated_fields.append("display_name")
|
||||
|
||||
if avatar_url and user.avatar != avatar_url:
|
||||
user.avatar = avatar_url
|
||||
user.save(update_fields=["avatar", "updated_at"])
|
||||
updated_fields.append("avatar")
|
||||
|
||||
if updated_fields:
|
||||
updated_fields.append("updated_at")
|
||||
user.save(update_fields=updated_fields)
|
||||
|
||||
return updated_fields
|
||||
|
||||
|
||||
def sync_user_avatar_from_payload(user, payload):
|
||||
sync_user_profile_from_payload(user, payload)
|
||||
|
||||
|
||||
def serialize_project(project):
|
||||
@@ -328,6 +344,37 @@ class NodeDCInternalWorkspaceListEndpoint(View):
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCInternalUserProfileSyncEndpoint(View):
|
||||
def post(self, request):
|
||||
if not is_internal_logout_request_authorized(request):
|
||||
return internal_unauthorized_response()
|
||||
|
||||
payload = parse_json_body(request)
|
||||
if payload is None:
|
||||
return JsonResponse({"ok": False, "error": "invalid_json"}, status=400)
|
||||
|
||||
user = resolve_user(payload)
|
||||
if user is None:
|
||||
return JsonResponse({"ok": False, "error": "user_not_found"}, status=404)
|
||||
|
||||
with transaction.atomic():
|
||||
updated_fields = sync_user_profile_from_payload(user, payload)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
"updatedFields": updated_fields,
|
||||
"user": {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"displayName": user.display_name,
|
||||
"avatar": user.avatar or None,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCInternalWorkspaceMembershipEnsureEndpoint(View):
|
||||
def post(self, request):
|
||||
|
||||
Reference in New Issue
Block a user