44import java .security .GeneralSecurityException ;
55import java .sql .SQLException ;
66import java .util .ArrayList ;
7+ import java .util .HashMap ;
78import java .util .List ;
9+ import java .util .Map ;
810import java .util .Optional ;
911import com .fasterxml .jackson .core .JsonProcessingException ;
1012import com .fasterxml .jackson .databind .node .ObjectNode ;
1820import io .sentrius .sso .core .model .hostgroup .ProfileConfiguration ;
1921import io .sentrius .sso .core .model .metadata .TerminalSessionMetadata ;
2022import io .sentrius .sso .core .model .security .enums .SSHAccessEnum ;
23+ import io .sentrius .sso .core .model .users .User ;
2124import io .sentrius .sso .core .services .ErrorOutputService ;
2225import io .sentrius .sso .core .services .HostGroupService ;
2326import io .sentrius .sso .core .services .SessionService ;
2427import io .sentrius .sso .core .services .TerminalService ;
2528import io .sentrius .sso .core .services .UserService ;
2629import io .sentrius .sso .core .services .metadata .TerminalSessionMetadataService ;
2730import io .sentrius .sso .core .services .security .CryptoService ;
31+ import io .sentrius .sso .core .services .security .ZtatTokenService ;
2832import io .sentrius .sso .core .utils .AccessUtil ;
2933import io .sentrius .sso .core .utils .JsonUtil ;
3034import jakarta .servlet .http .HttpServletRequest ;
3135import jakarta .servlet .http .HttpServletResponse ;
3236import lombok .extern .slf4j .Slf4j ;
3337import org .hibernate .Hibernate ;
38+ import org .springframework .http .HttpHeaders ;
39+ import org .springframework .http .MediaType ;
3440import org .springframework .http .ResponseEntity ;
3541import org .springframework .stereotype .Controller ;
3642import org .springframework .web .bind .annotation .GetMapping ;
@@ -50,6 +56,7 @@ public class HostApiController extends BaseController {
5056 final TerminalService terminalService ;
5157 final SessionService sessionService ;
5258 final CryptoService cryptoService ;
59+ final ZtatTokenService ztatTokenService ;
5360 final TerminalSessionMetadataService terminalSessionMetadataService ;
5461
5562 protected HostApiController (
@@ -59,13 +66,14 @@ protected HostApiController(
5966 HostGroupService hostGroupService ,
6067 TerminalService terminalService ,
6168 SessionService sessionService ,
62- CryptoService cryptoService ,
69+ CryptoService cryptoService , ZtatTokenService ztatTokenService ,
6370 TerminalSessionMetadataService terminalSessionMetadataService ) {
6471 super (userService , systemOptions , errorOutputService );
6572 this .hostGroupService = hostGroupService ;
6673 this .terminalService = terminalService ;
6774 this .sessionService = sessionService ;
6875 this .cryptoService = cryptoService ;
76+ this .ztatTokenService = ztatTokenService ;
6977 this .terminalSessionMetadataService = terminalSessionMetadataService ;
7078 }
7179
@@ -294,4 +302,205 @@ public ResponseEntity<ObjectNode> connectSSHServer(HttpServletRequest request, H
294302 return ResponseEntity .ok (node );
295303 }
296304
305+ @ GetMapping ("/rdp/connect/{enclave}/{host_id}" )
306+ @ LimitAccess (sshAccess = {SSHAccessEnum .CAN_MANAGE_SYSTEMS }, endpointThreat = EndpointThreat .HIGH )
307+ public ResponseEntity <Map <String , Object >> initiateRdpSession (
308+ HttpServletRequest request ,
309+ HttpServletResponse response ,
310+ @ PathVariable ("enclave" ) Long enclaveId ,
311+ @ PathVariable ("host_id" ) Long hostId ) {
312+
313+ try {
314+ User user = getOperatingUser (request , response );
315+
316+ // Validate access to the host group
317+ Optional <HostGroup > hostGroupOpt = hostGroupService .getHostGroupWithHostSystems (user , enclaveId );
318+ if (hostGroupOpt .isEmpty ()) {
319+ // log.warn("User {} does not have access to host group {}", user.getUsername(), enclaveId);
320+ return ResponseEntity .badRequest ().build ();
321+ }
322+
323+ // Get the host system
324+ Optional <HostSystem > hostSystemOpt = hostGroupService .getHostSystem (hostId );
325+ if (hostSystemOpt .isEmpty ()) {
326+ // log.warn("Host system {} not found", hostId);
327+ return ResponseEntity .notFound ().build ();
328+ }
329+
330+ HostSystem hostSystem = hostSystemOpt .get ();
331+
332+ // Check if RDP is enabled for this host
333+ if (!hostSystem .isRdpEnabled ()) {
334+ // log.warn("RDP is not enabled for host system {}", hostId);
335+ return ResponseEntity .badRequest ().build ();
336+ }
337+
338+ // Generate JWT token for this user and target
339+ String jwtToken = generateRdpJwtToken (user , hostSystem .getDisplayName ());
340+ if (jwtToken == null ) {
341+ // log.error("Failed to generate JWT token for user {} and target {}", user.getUsername(), hostSystem.getDisplayName());
342+ return ResponseEntity .internalServerError ().build ();
343+ }
344+
345+ // Create RDP session data
346+ Map <String , Object > sessionData = new HashMap <>();
347+ sessionData .put ("host" , hostSystem .getHost ());
348+ sessionData .put ("port" , hostSystem .getRdpPort () != null ? hostSystem .getRdpPort () : 3389 );
349+ sessionData .put ("username" , user .getUsername ());
350+ sessionData .put ("jwtToken" , jwtToken );
351+ sessionData .put ("target" , hostSystem .getDisplayName ());
352+ sessionData .put ("websocketHost" , systemOptions .getRdpProxyDomain ());
353+ sessionData .put ("websocketUrl" , "/guacamole/tunnel?token=" + jwtToken );
354+ sessionData .put ("displayName" , hostSystem .getDisplayName ());
355+
356+ // log.info("Initiated RDP session for user {} to connect to host {}", user.getUsername(), hostSystem.getDisplayName());
357+
358+ return ResponseEntity .ok (sessionData );
359+
360+ } catch (Exception e ) {
361+ // log.error("Error initiating RDP session for enclave {} and host {}", enclaveId, hostId, e);
362+ return ResponseEntity .internalServerError ().build ();
363+ }
364+ }
365+
366+ @ GetMapping ("/rdp/download/{enclave}/{host_id}" )
367+ @ LimitAccess (sshAccess = {SSHAccessEnum .CAN_MANAGE_SYSTEMS }, endpointThreat = EndpointThreat .HIGH )
368+ public ResponseEntity <String > downloadRdpFile (
369+ HttpServletRequest request ,
370+ HttpServletResponse response ,
371+ @ PathVariable ("enclave" ) Long enclaveId ,
372+ @ PathVariable ("host_id" ) Long hostId ) {
373+
374+ try {
375+ User user = getOperatingUser (request , response );
376+
377+ // Validate access to the host group
378+ Optional <HostGroup > hostGroupOpt = hostGroupService .getHostGroupWithHostSystems (user , enclaveId );
379+ if (hostGroupOpt .isEmpty ()) {
380+ // log.warn("User {} does not have access to host group {}", user.getUsername(), enclaveId);
381+ return ResponseEntity .badRequest ().build ();
382+ }
383+
384+ // Get the host system
385+ Optional <HostSystem > hostSystemOpt = hostGroupService .getHostSystem (hostId );
386+ if (hostSystemOpt .isEmpty ()) {
387+ // log.warn("Host system {} not found", hostId);
388+ return ResponseEntity .notFound ().build ();
389+ }
390+
391+ HostSystem hostSystem = hostSystemOpt .get ();
392+
393+ // Check if RDP is enabled for this host
394+ if (!hostSystem .isRdpEnabled ()) {
395+ // log.warn("RDP is not enabled for host system {}", hostId);
396+ return ResponseEntity .badRequest ().build ();
397+ }
398+
399+ // Generate JWT token for this user and target
400+ String jwtToken = generateRdpJwtToken (user , hostSystem .getDisplayName ());
401+ if (jwtToken == null ) {
402+ // log.error("Failed to generate JWT token for user {} and target {}", user.getUsername(), hostSystem.getDisplayName());
403+ return ResponseEntity .internalServerError ().build ();
404+ }
405+
406+ // Generate RDP file content
407+ String rdpFileContent = generateRdpFileContent (hostSystem ,user , jwtToken );
408+
409+ // Set response headers for file download
410+ HttpHeaders headers = new HttpHeaders ();
411+ headers .setContentType (new MediaType ("application" , "rdp" ));
412+ headers .setContentDispositionFormData ("attachment" , hostSystem .getDisplayName () + ".rdp" );
413+
414+ // log.info("Generated RDP file for user {} to connect to host {}", user.getUsername(), hostSystem.getDisplayName());
415+
416+ return ResponseEntity .ok ()
417+ .headers (headers )
418+ .body (rdpFileContent );
419+
420+ } catch (Exception e ) {
421+ // log.error("Error generating RDP file for enclave {} and host {}", enclaveId, hostId, e);
422+ return ResponseEntity .internalServerError ().build ();
423+ }
424+ }
425+
426+ /**
427+ * Generate a JWT token for RDP authentication
428+ */
429+ private String generateRdpJwtToken (User user , String target ) {
430+ try {
431+ // log.info("Generating JWT token for user {} and target {}", user.getUsername(), target);
432+
433+ return ztatTokenService .issueServiceToken (user .getUsername (), "rdp-proxy" , target , 60 );
434+
435+
436+ } catch (Exception e ) {
437+ // log.error("Error generating JWT token for user {} and target {}", user.getUsername(), target, e);
438+ return null ;
439+ }
440+ }
441+
442+ /**
443+ * Generate RDP file content for a host system
444+ */
445+ private String generateRdpFileContent (HostSystem hostSystem , User user , String jwtToken ) {
446+ StringBuilder rdpContent = new StringBuilder ();
447+
448+ // Basic RDP file format
449+ rdpContent .append ("screen mode id:i:2\n " );
450+ rdpContent .append ("use multimon:i:0\n " );
451+ rdpContent .append ("desktopwidth:i:1920\n " );
452+ rdpContent .append ("desktopheight:i:1080\n " );
453+ rdpContent .append ("session bpp:i:32\n " );
454+ rdpContent .append ("winposstr:s:0,3,0,0,800,600\n " );
455+ rdpContent .append ("compression:i:1\n " );
456+ rdpContent .append ("keyboardhook:i:2\n " );
457+ rdpContent .append ("audiocapturemode:i:0\n " );
458+ rdpContent .append ("videoplaybackmode:i:1\n " );
459+ rdpContent .append ("connection type:i:7\n " );
460+ rdpContent .append ("networkautodetect:i:1\n " );
461+ rdpContent .append ("bandwidthautodetect:i:1\n " );
462+ rdpContent .append ("displayconnectionbar:i:1\n " );
463+ rdpContent .append ("enableworkspacereconnect:i:0\n " );
464+ rdpContent .append ("disable wallpaper:i:0\n " );
465+ rdpContent .append ("allow font smoothing:i:0\n " );
466+ rdpContent .append ("allow desktop composition:i:0\n " );
467+ rdpContent .append ("disable full window drag:i:1\n " );
468+ rdpContent .append ("disable menu anims:i:1\n " );
469+ rdpContent .append ("disable themes:i:0\n " );
470+ rdpContent .append ("disable cursor setting:i:0\n " );
471+ rdpContent .append ("bitmapcachepersistenable:i:1\n " );
472+
473+ // Connection details - point to the RDP proxy (default values)
474+ String rdpProxyHost = "agentproxy-dev.local" ; // This should be configurable
475+ int rdpProxyPort = 30089 ; // This should be configurable
476+ rdpContent .append ("full address:s:" ).append (rdpProxyHost ).append (":" ).append (rdpProxyPort ).append ("\n " );
477+
478+ // Authentication details - use JWT token in password field
479+ rdpContent .append ("username:s:" ).append (user .getUsername ()).append ("\n " );
480+ rdpContent .append ("domain:s:\n " );
481+ rdpContent .append ("password:s:__token__:" ).append (jwtToken ).append ("\n " );
482+
483+ // Security settings - disable clipboard and drive redirection by default
484+ rdpContent .append ("redirectclipboard:i:0\n " );
485+ rdpContent .append ("redirectdrives:i:0\n " );
486+ rdpContent .append ("redirectcomports:i:0\n " );
487+ rdpContent .append ("redirectsmartcards:i:0\n " );
488+ rdpContent .append ("redirectprinters:i:0\n " );
489+
490+ // Additional settings
491+ rdpContent .append ("alternate shell:s:\n " );
492+ rdpContent .append ("shell working directory:s:\n " );
493+ rdpContent .append ("gatewayhostname:s:\n " );
494+ rdpContent .append ("gatewayusagemethod:i:4\n " );
495+ rdpContent .append ("gatewaycredentialssource:i:4\n " );
496+ rdpContent .append ("gatewayprofileusagemethod:i:0\n " );
497+ rdpContent .append ("promptcredentialonce:i:0\n " );
498+ rdpContent .append ("gatewaybrokeringtype:i:0\n " );
499+ rdpContent .append ("use redirection server name:i:0\n " );
500+ rdpContent .append ("rdgiskdcproxy:i:0\n " );
501+ rdpContent .append ("kdcproxyname:s:\n " );
502+
503+ return rdpContent .toString ();
504+ }
505+
297506}
0 commit comments