@@ -23,6 +23,7 @@ import (
2323 "github.com/flatrun/agent/internal/routing"
2424 "github.com/flatrun/agent/internal/system"
2525 "github.com/flatrun/agent/pkg/config"
26+ "github.com/flatrun/agent/pkg/models"
2627 "github.com/flatrun/agent/pkg/version"
2728 "github.com/gin-gonic/gin"
2829)
@@ -709,6 +710,19 @@ func clusterPolicyAccess(policy cluster.PeerPolicy) ([]string, auth.DeploymentAc
709710 permissions [auth .PermContainersRead .String ()] = true
710711 permissions [auth .PermContainersWrite .String ()] = true
711712 unrestrictedDeployments = mergeClusterDeploymentAccess (deployments , grant .Deployments , auth .AccessLevelWrite , unrestrictedDeployments )
713+ case cluster .CapabilityDeploymentsManage :
714+ for _ , permission := range []auth.Permission {
715+ auth .PermDeploymentsRead , auth .PermDeploymentsWrite , auth .PermDeploymentsDelete ,
716+ auth .PermContainersRead , auth .PermContainersWrite , auth .PermContainersDelete ,
717+ auth .PermCertificatesRead , auth .PermCertificatesWrite , auth .PermCertificatesDelete ,
718+ auth .PermSecurityRead , auth .PermSecurityWrite , auth .PermImagesRead ,
719+ auth .PermImagesWrite , auth .PermImagesDelete , auth .PermBackupsRead ,
720+ auth .PermBackupsWrite , auth .PermBackupsDelete ,
721+ auth .PermSchedulerRead , auth .PermSchedulerWrite , auth .PermSchedulerDelete ,
722+ } {
723+ permissions [permission .String ()] = true
724+ }
725+ unrestrictedDeployments = mergeClusterDeploymentAccess (deployments , grant .Deployments , auth .AccessLevelAdmin , unrestrictedDeployments )
712726 case cluster .CapabilityCapacityRead :
713727 permissions [auth .PermSystemRead .String ()] = true
714728 case cluster .CapabilityCapacityOffer :
@@ -734,13 +748,26 @@ func mergeClusterDeploymentAccess(access auth.DeploymentAccess, names []string,
734748 return true
735749 }
736750 for _ , name := range names {
737- if current , ok := access [name ]; ! ok || current == auth . AccessLevelRead && level == auth . AccessLevelWrite {
751+ if current , ok := access [name ]; ! ok || clusterAccessLevelRank ( level ) > clusterAccessLevelRank ( current ) {
738752 access [name ] = level
739753 }
740754 }
741755 return false
742756}
743757
758+ func clusterAccessLevelRank (level string ) int {
759+ switch level {
760+ case auth .AccessLevelRead :
761+ return 1
762+ case auth .AccessLevelWrite :
763+ return 2
764+ case auth .AccessLevelAdmin :
765+ return 3
766+ default :
767+ return 0
768+ }
769+ }
770+
744771func (s * Server ) applyClusterPeerPolicy (policy cluster.PeerPolicy ) error {
745772 if s .authManager == nil {
746773 return fmt .Errorf ("Authentication manager is not available" )
@@ -833,6 +860,9 @@ func (s *Server) deleteClusterAPIKey(peerName string) error {
833860}
834861
835862func (s * Server ) clusterProxy (c * gin.Context ) {
863+ if ! authorizePeerProxy (c ) {
864+ return
865+ }
836866 mgr := s .getClusterManager ()
837867 if mgr == nil {
838868 c .JSON (http .StatusBadRequest , gin.H {"error" : "Cluster is not enabled" })
@@ -853,18 +883,143 @@ func (s *Server) clusterProxy(c *gin.Context) {
853883 body = c .Request .Body
854884 }
855885
856- data , status , headers , err := client .Forward (c .Request .Context (), c .Request .Method , "/api" + path , body )
886+ forwardPath := "/api" + path
887+ if c .Request .URL .RawQuery != "" {
888+ forwardPath += "?" + c .Request .URL .RawQuery
889+ }
890+ resp , err := client .DoWithHeaders (c .Request .Context (), c .Request .Method , forwardPath , c .Request .Header , body )
857891 if err != nil {
858892 c .JSON (http .StatusBadGateway , gin.H {"error" : fmt .Sprintf ("Failed to proxy request: %v" , err )})
859893 return
860894 }
861895
862- for k , v := range headers {
863- if k != "Content-Length" && k != "Transfer-Encoding" {
864- c .Header (k , v )
896+ defer resp .Body .Close ()
897+ for k , values := range resp .Header {
898+ if k != "Content-Length" && k != "Transfer-Encoding" && k != "Connection" {
899+ for _ , value := range values {
900+ c .Writer .Header ().Add (k , value )
901+ }
902+ }
903+ }
904+ if c .Request .Method == http .MethodGet && path == "/deployments" {
905+ s .writeScopedPeerDeployments (c , name , resp )
906+ return
907+ }
908+ c .Status (resp .StatusCode )
909+ _ , _ = io .Copy (c .Writer , resp .Body )
910+ }
911+
912+ func (s * Server ) writeScopedPeerDeployments (c * gin.Context , peer string , resp * http.Response ) {
913+ actor := auth .GetActorFromContext (c )
914+ if actor == nil || actor .Role == auth .RoleAdmin || resp .StatusCode != http .StatusOK {
915+ c .Status (resp .StatusCode )
916+ _ , _ = io .Copy (c .Writer , resp .Body )
917+ return
918+ }
919+ var payload struct {
920+ Deployments []models.Deployment `json:"deployments"`
921+ Path string `json:"path,omitempty"`
922+ }
923+ if err := json .NewDecoder (resp .Body ).Decode (& payload ); err != nil {
924+ c .JSON (http .StatusBadGateway , gin.H {"error" : "Peer returned an invalid deployment list" })
925+ return
926+ }
927+ visible := payload .Deployments [:0 ]
928+ for _ , deployment := range payload .Deployments {
929+ if actor .CanAccessPeerDeployment (peer , deployment .Name , auth .AccessLevelRead ) {
930+ visible = append (visible , deployment )
931+ }
932+ }
933+ payload .Deployments = visible
934+ c .JSON (http .StatusOK , payload )
935+ }
936+
937+ func authorizePeerProxy (c * gin.Context ) bool {
938+ actor := auth .GetActorFromContext (c )
939+ if actor == nil {
940+ c .JSON (http .StatusUnauthorized , gin.H {"error" : "Not authenticated" })
941+ return false
942+ }
943+
944+ path := c .Param ("path" )
945+ peer := c .Param ("name" )
946+ deployment := peerProxyDeployment (path )
947+ if deployment == "" {
948+ deployment = strings .TrimSpace (c .GetHeader ("X-FlatRun-Deployment" ))
949+ }
950+ if actor .Role != auth .RoleAdmin && path != "/deployments" && deployment == "" {
951+ c .JSON (http .StatusForbidden , gin.H {"error" : "A deployment scope is required" })
952+ return false
953+ }
954+ requiredLevel := auth .AccessLevelRead
955+ if c .Request .Method != http .MethodGet && c .Request .Method != http .MethodHead {
956+ requiredLevel = auth .AccessLevelWrite
957+ }
958+ if c .Request .Method == http .MethodDelete {
959+ requiredLevel = auth .AccessLevelAdmin
960+ }
961+ if deployment != "" && ! actor .CanAccessPeerDeployment (peer , deployment , requiredLevel ) {
962+ c .JSON (http .StatusForbidden , gin.H {"error" : "No access to this peer deployment" })
963+ return false
964+ }
965+ permission := auth .PermDeploymentsRead
966+ if c .Request .Method != http .MethodGet && c .Request .Method != http .MethodHead {
967+ permission = auth .PermDeploymentsWrite
968+ }
969+ if c .Request .Method == http .MethodDelete {
970+ permission = auth .PermDeploymentsDelete
971+ }
972+
973+ switch {
974+ case strings .HasPrefix (path , "/containers/" ):
975+ permission = auth .PermContainersRead
976+ if c .Request .Method != http .MethodGet && c .Request .Method != http .MethodHead {
977+ permission = auth .PermContainersWrite
978+ }
979+ if c .Request .Method == http .MethodDelete {
980+ permission = auth .PermContainersDelete
981+ }
982+ case strings .HasPrefix (path , "/certificates" ), strings .HasPrefix (path , "/proxy/" ):
983+ permission = auth .PermCertificatesRead
984+ if c .Request .Method != http .MethodGet && c .Request .Method != http .MethodHead {
985+ permission = auth .PermCertificatesWrite
986+ }
987+ if c .Request .Method == http .MethodDelete {
988+ permission = auth .PermCertificatesDelete
989+ }
990+ case strings .Contains (path , "/security" ):
991+ permission = auth .PermSecurityRead
992+ if c .Request .Method != http .MethodGet && c .Request .Method != http .MethodHead {
993+ permission = auth .PermSecurityWrite
994+ }
995+ case strings .HasPrefix (path , "/backups" ), strings .Contains (path , "/backups" ):
996+ permission = auth .PermBackupsRead
997+ if c .Request .Method != http .MethodGet && c .Request .Method != http .MethodHead {
998+ permission = auth .PermBackupsWrite
999+ }
1000+ if c .Request .Method == http .MethodDelete {
1001+ permission = auth .PermBackupsDelete
1002+ }
1003+ case strings .HasPrefix (path , "/credentials" ):
1004+ permission = auth .PermRegistriesRead
1005+ }
1006+
1007+ if ! actor .HasPermission (permission ) {
1008+ c .JSON (http .StatusForbidden , gin.H {"error" : "Permission denied" , "required" : permission })
1009+ return false
1010+ }
1011+ return true
1012+ }
1013+
1014+ func peerProxyDeployment (path string ) string {
1015+ parts := strings .Split (strings .Trim (path , "/" ), "/" )
1016+ if len (parts ) >= 2 && parts [0 ] == "deployments" {
1017+ name , err := url .PathUnescape (parts [1 ])
1018+ if err == nil {
1019+ return name
8651020 }
8661021 }
867- c . Data ( status , "application/json" , data )
1022+ return ""
8681023}
8691024
8701025func (s * Server ) clusterAggregateDeployments (c * gin.Context ) {
0 commit comments