@@ -747,6 +747,95 @@ func TestNetworkServerSessionAssetDeleteMissingIsIdempotent(t *testing.T) {
747747 }
748748}
749749
750+ // TestNetworkServerSessionAssetACLIndependent 验证 GET 和 DELETE 的 ACL 检查相互独立:
751+ // 只允许 read 时 GET 通过但 DELETE 被拒;只允许 delete 时 DELETE 通过但 GET 被拒。
752+ func TestNetworkServerSessionAssetACLIndependent (t * testing.T ) {
753+ t .Run ("read allowed delete denied" , func (t * testing.T ) {
754+ readOnlyACL := & ControlPlaneACL {
755+ mode : ACLModeStrict ,
756+ allow : map [RequestSource ]map [string ]struct {}{RequestSourceHTTP : {sessionAssetReadMethod : {}}},
757+ enabled : true ,
758+ }
759+ runtimePort := & runtimePortEventStub {
760+ openAssetFn : func (context.Context , OpenSessionAssetInput ) (OpenSessionAssetResult , error ) {
761+ return OpenSessionAssetResult {
762+ Reader : io .NopCloser (bytes .NewReader (gatewayMinimalPNGBytes ())),
763+ Meta : SessionAssetMeta {SessionID : "session-1" , AssetID : "asset-1" , MimeType : "image/png" },
764+ }, nil
765+ },
766+ deleteAssetFn : func (context.Context , DeleteSessionAssetInput ) error {
767+ t .Fatal ("DeleteSessionAsset should not be called when ACL denies delete" )
768+ return nil
769+ },
770+ }
771+ server := & NetworkServer {
772+ authenticator : staticTokenAuthenticator {token : "gateway-token" },
773+ acl : readOnlyACL ,
774+ metrics : NewGatewayMetrics (),
775+ }
776+ handler := server .buildHandler (runtimePort )
777+
778+ // GET should succeed (read allowed)
779+ readRequest := httptest .NewRequest (http .MethodGet , "/api/session-assets/session-1/asset-1" , nil )
780+ readRequest .Header .Set ("Authorization" , "Bearer gateway-token" )
781+ readRecorder := httptest .NewRecorder ()
782+ handler .ServeHTTP (readRecorder , readRequest )
783+ if readRecorder .Code != http .StatusOK {
784+ t .Fatalf ("read status = %d, want %d" , readRecorder .Code , http .StatusOK )
785+ }
786+
787+ // DELETE should be forbidden (delete denied)
788+ deleteRequest := httptest .NewRequest (http .MethodDelete , "/api/session-assets/session-1/asset-1" , nil )
789+ deleteRequest .Header .Set ("Authorization" , "Bearer gateway-token" )
790+ deleteRecorder := httptest .NewRecorder ()
791+ handler .ServeHTTP (deleteRecorder , deleteRequest )
792+ if deleteRecorder .Code != http .StatusForbidden {
793+ t .Fatalf ("delete status = %d, want %d" , deleteRecorder .Code , http .StatusForbidden )
794+ }
795+ })
796+
797+ t .Run ("delete allowed read denied" , func (t * testing.T ) {
798+ deleteOnlyACL := & ControlPlaneACL {
799+ mode : ACLModeStrict ,
800+ allow : map [RequestSource ]map [string ]struct {}{RequestSourceHTTP : {sessionAssetDeleteMethod : {}}},
801+ enabled : true ,
802+ }
803+ runtimePort := & runtimePortEventStub {
804+ openAssetFn : func (context.Context , OpenSessionAssetInput ) (OpenSessionAssetResult , error ) {
805+ t .Fatal ("OpenSessionAsset should not be called when ACL denies read" )
806+ return OpenSessionAssetResult {}, nil
807+ },
808+ deleteAssetFn : func (context.Context , DeleteSessionAssetInput ) error {
809+ return nil
810+ },
811+ }
812+ server := & NetworkServer {
813+ authenticator : staticTokenAuthenticator {token : "gateway-token" },
814+ acl : deleteOnlyACL ,
815+ metrics : NewGatewayMetrics (),
816+ }
817+ handler := server .buildHandler (runtimePort )
818+
819+ // DELETE should succeed (delete allowed)
820+ deleteRequest := httptest .NewRequest (http .MethodDelete , "/api/session-assets/session-1/asset-1" , nil )
821+ deleteRequest .Header .Set ("Authorization" , "Bearer gateway-token" )
822+ deleteRecorder := httptest .NewRecorder ()
823+ handler .ServeHTTP (deleteRecorder , deleteRequest )
824+ if deleteRecorder .Code != http .StatusOK {
825+ t .Fatalf ("delete status = %d, want %d" , deleteRecorder .Code , http .StatusOK )
826+ }
827+
828+ // GET should be forbidden (read denied)
829+ readRequest := httptest .NewRequest (http .MethodGet , "/api/session-assets/session-1/asset-1" , nil )
830+ readRequest .Header .Set ("Authorization" , "Bearer gateway-token" )
831+ readRecorder := httptest .NewRecorder ()
832+ handler .ServeHTTP (readRecorder , readRequest )
833+ if readRecorder .Code != http .StatusForbidden {
834+ t .Fatalf ("read status = %d, want %d" , readRecorder .Code , http .StatusForbidden )
835+ }
836+ })
837+ }
838+
750839func TestNetworkServerSessionAssetsRequireAssetPort (t * testing.T ) {
751840 runtimePort := & runtimePortWithoutSessionAsset {RuntimePort : & runtimePortEventStub {}}
752841 server := & NetworkServer {authenticator : staticTokenAuthenticator {token : "gateway-token" }}
0 commit comments