From afe352cdb1e05e48ca03bb35ab5c077bbca934ac Mon Sep 17 00:00:00 2001 From: erosente Date: Wed, 24 Sep 2025 13:01:59 -0400 Subject: [PATCH 1/2] Initial working commit for protected labels. --- api/v1/openapi.yaml | 8 ++++ go.mod | 9 ++++ go.sum | 95 +++++++++++++++++++++++++++++++++++++ internal/api/server.go | 70 +++++++++++++++++++++++++-- internal/api/server_test.go | 88 +++++++++++++++++++++++++++++----- pkg/apis/v1/types.go | 61 ++++++++++++++---------- 6 files changed, 291 insertions(+), 40 deletions(-) diff --git a/api/v1/openapi.yaml b/api/v1/openapi.yaml index 832891a..79fc73e 100644 --- a/api/v1/openapi.yaml +++ b/api/v1/openapi.yaml @@ -106,6 +106,12 @@ paths: application/json: schema: $ref: "#/components/schemas/WarningResponse" + "403": + description: Forbidden - attempt to modify protected system labels. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' "400": description: Invalid request parameters. content: @@ -213,6 +219,8 @@ components: properties: status: $ref: '#/components/schemas/StatusSchema' + labels: + $ref: '#/components/schemas/LabelsSchema' StatusSchema: type: string diff --git a/go.mod b/go.mod index 6895e2d..516041f 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -53,6 +54,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -64,20 +66,27 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/sagikazarmark/locafero v0.10.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.9.2 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/x448/float16 v0.8.4 // indirect + golang.org/x/mod v0.25.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect diff --git a/go.sum b/go.sum index bf0bd96..d673d8d 100644 --- a/go.sum +++ b/go.sum @@ -6,14 +6,22 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -50,6 +58,7 @@ github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zib github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= @@ -58,18 +67,34 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -79,8 +104,11 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -94,16 +122,30 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/nethttp-middleware v1.1.2 h1:TQwEU3WM6ifc7ObBEtiJgbRPaCe513tvJpiMJjypVPA= github.com/oapi-codegen/nethttp-middleware v1.1.2/go.mod h1:5qzjxMSiI8HjLljiOEjvs4RdrWyMPKnExeFS2kr8om4= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w= github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -127,8 +169,13 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc= github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= +github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= +github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= +github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= @@ -144,12 +191,16 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -159,26 +210,50 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= @@ -186,6 +261,7 @@ golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= @@ -193,15 +269,34 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk= diff --git a/internal/api/server.go b/internal/api/server.go index 81cd343..078bcc5 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -17,8 +17,10 @@ import ( ) const ( - baseAppLabelKey = "app" - baseAppLabelValue = "rhobs-synthetics-probe" + baseAppLabelKey = "app" + baseAppLabelValue = "rhobs-synthetics-probe" + probeStatusLabelKey = "rhobs-synthetics/status" + probeURLHashLabelKey = "rhobs-synthetics/static-url-hash" ) // Server is the main API server object. @@ -33,6 +35,45 @@ func NewServer(store probestore.ProbeStorage) Server { } } +// validateProtectedLabels checks if the user is trying to modify protected system labels +func validateProtectedLabels(labels *v1.LabelsSchema) error { + if labels == nil { + return nil + } + + protectedLabels := []string{ + baseAppLabelKey, // "app" + probeStatusLabelKey, // "rhobs-synthetics/status" + probeURLHashLabelKey, // "rhobs-synthetics/static-url-hash" + } + + for _, protectedLabel := range protectedLabels { + if _, exists := (*labels)[protectedLabel]; exists { + return fmt.Errorf("modification of system-managed label '%s' is forbidden", protectedLabel) + } + } + + return nil +} + +// validateProtectedFields checks if the user is trying to modify protected fields/labels +func validateProtectedFields(request *v1.UpdateProbeJSONRequestBody) error { + // Check if user is trying to modify protected labels + if err := validateProtectedLabels(request.Labels); err != nil { + return err + } + + // Check if user is trying to modify status (except for deletion) + if request.Status != nil { + // Only allow status change to "deleted" for deletion functionality + if *request.Status != v1.Deleted { + return fmt.Errorf("modification of status field is forbidden - it's managed by the system (only deletion via 'deleted' status is allowed)") + } + } + + return nil +} + // (GET /probes) func (s Server) ListProbes(ctx context.Context, request v1.ListProbesRequestObject) (v1.ListProbesResponseObject, error) { defer metrics.RecordProbestoreRequest("list_probes", time.Now()) @@ -131,6 +172,16 @@ func (s Server) CreateProbe(ctx context.Context, request v1.CreateProbeRequestOb // (PATCH /probes/{probe_id}) func (s Server) UpdateProbe(ctx context.Context, request v1.UpdateProbeRequestObject) (v1.UpdateProbeResponseObject, error) { defer metrics.RecordProbestoreRequest("update_probe", time.Now()) + + // Validate that protected fields/labels are not being modified - return 403 if they are + if err := validateProtectedFields(request.Body); err != nil { + return v1.UpdateProbe403JSONResponse{ + Error: v1.ErrorObject{ + Message: err.Error(), + }, + }, nil + } + // First, get the existing probe. existingProbe, err := s.Store.GetProbe(ctx, request.ProbeId) if err != nil { @@ -146,7 +197,7 @@ func (s Server) UpdateProbe(ctx context.Context, request v1.UpdateProbeRequestOb return nil, fmt.Errorf("failed to get probe from storage for update: %w", err) } - // Now, update the fields from the request. + // Handle status updates (only "deleted" is allowed, validated above) if request.Body.Status != nil { existingProbe.Status = *request.Body.Status @@ -163,6 +214,19 @@ func (s Server) UpdateProbe(ctx context.Context, request v1.UpdateProbeRequestOb } } + // Update user-provided labels (merge with existing labels) + if request.Body.Labels != nil { + // Initialize labels if they don't exist + if existingProbe.Labels == nil { + existingProbe.Labels = &v1.LabelsSchema{} + } + + // Merge the new labels with existing ones + for key, value := range *request.Body.Labels { + (*existingProbe.Labels)[key] = value + } + } + // Persist the updated probe (for non-deleted status changes). updatedProbe, err := s.Store.UpdateProbe(ctx, *existingProbe) if err != nil { diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 31763e4..12797b7 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -377,26 +377,20 @@ func TestUpdateProbe(t *testing.T) { postCheck func(t *testing.T, store probestore.ProbeStorage) }{ { - name: "successfully updates a probe", + name: "returns 403 when trying to update status field to non-deleted value", probeID: probeID, reqBody: v1.UpdateProbeJSONRequestBody{Status: &newStatus}, store: &mockProbeStore{ probes: map[uuid.UUID]v1.ProbeObject{probeID: initialProbe}, }, - expectedResponse: v1.UpdateProbe200JSONResponse{ - Id: probeID, - StaticUrl: "https://example.com", - Status: v1.Active, - }, - postCheck: func(t *testing.T, store probestore.ProbeStorage) { - s := store.(*mockProbeStore) - assert.Equal(t, newStatus, s.probes[probeID].Status) + expectedResponse: v1.UpdateProbe403JSONResponse{ + Error: v1.ErrorObject{Message: "modification of status field is forbidden - it's managed by the system (only deletion via 'deleted' status is allowed)"}, }, }, { - name: "returns 404 when probe does not exist", + name: "returns 404 when probe does not exist (testing with labels)", probeID: uuid.New(), - reqBody: v1.UpdateProbeJSONRequestBody{Status: &newStatus}, + reqBody: v1.UpdateProbeJSONRequestBody{Labels: &v1.LabelsSchema{"environment": "test"}}, store: &mockProbeStore{probes: map[uuid.UUID]v1.ProbeObject{}}, expectedResponse: v1.UpdateProbe404JSONResponse{ Warning: v1.WarningObject{Message: fmt.Sprintf("probe with ID %s not found", uuid.New().String())}, // Message is dynamic, we'll check the type @@ -405,7 +399,7 @@ func TestUpdateProbe(t *testing.T) { { name: "returns error when getting probe fails", probeID: probeID, - reqBody: v1.UpdateProbeJSONRequestBody{Status: &newStatus}, + reqBody: v1.UpdateProbeJSONRequestBody{Labels: &v1.LabelsSchema{"environment": "test"}}, store: &mockProbeStore{ probes: map[uuid.UUID]v1.ProbeObject{probeID: initialProbe}, getProbeErr: errors.New("generic get error"), @@ -415,7 +409,7 @@ func TestUpdateProbe(t *testing.T) { { name: "returns error when updating probe fails", probeID: probeID, - reqBody: v1.UpdateProbeJSONRequestBody{Status: &newStatus}, + reqBody: v1.UpdateProbeJSONRequestBody{Labels: &v1.LabelsSchema{"environment": "test"}}, store: &mockProbeStore{ probes: map[uuid.UUID]v1.ProbeObject{probeID: initialProbe}, updateProbeErr: errors.New("generic update error"), @@ -443,6 +437,74 @@ func TestUpdateProbe(t *testing.T) { assert.False(t, exists, "Probe should have been actually deleted from store") }, }, + { + name: "successfully updates user labels", + probeID: probeID, + reqBody: v1.UpdateProbeJSONRequestBody{Labels: &v1.LabelsSchema{"environment": "prod", "team": "sre"}}, + store: &mockProbeStore{ + probes: map[uuid.UUID]v1.ProbeObject{probeID: initialProbe}, + }, + expectedResponse: v1.UpdateProbe200JSONResponse{ + Id: probeID, + StaticUrl: "https://example.com", + Status: v1.Pending, + Labels: &v1.LabelsSchema{"environment": "prod", "team": "sre"}, + }, + postCheck: func(t *testing.T, store probestore.ProbeStorage) { + s := store.(*mockProbeStore) + labels := s.probes[probeID].Labels + assert.NotNil(t, labels) + assert.Equal(t, "prod", (*labels)["environment"]) + assert.Equal(t, "sre", (*labels)["team"]) + }, + }, + { + name: "returns 403 when trying to modify protected label: app", + probeID: probeID, + reqBody: v1.UpdateProbeJSONRequestBody{Labels: &v1.LabelsSchema{"app": "malicious-app"}}, + store: &mockProbeStore{ + probes: map[uuid.UUID]v1.ProbeObject{probeID: initialProbe}, + }, + expectedResponse: v1.UpdateProbe403JSONResponse{ + Error: v1.ErrorObject{Message: "modification of system-managed label 'app' is forbidden"}, + }, + }, + { + name: "returns 403 when trying to modify protected label: rhobs-synthetics/status", + probeID: probeID, + reqBody: v1.UpdateProbeJSONRequestBody{Labels: &v1.LabelsSchema{"rhobs-synthetics/status": "hacked"}}, + store: &mockProbeStore{ + probes: map[uuid.UUID]v1.ProbeObject{probeID: initialProbe}, + }, + expectedResponse: v1.UpdateProbe403JSONResponse{ + Error: v1.ErrorObject{Message: "modification of system-managed label 'rhobs-synthetics/status' is forbidden"}, + }, + }, + { + name: "returns 403 when trying to modify protected label: rhobs-synthetics/static-url-hash", + probeID: probeID, + reqBody: v1.UpdateProbeJSONRequestBody{Labels: &v1.LabelsSchema{"rhobs-synthetics/static-url-hash": "fakehash"}}, + store: &mockProbeStore{ + probes: map[uuid.UUID]v1.ProbeObject{probeID: initialProbe}, + }, + expectedResponse: v1.UpdateProbe403JSONResponse{ + Error: v1.ErrorObject{Message: "modification of system-managed label 'rhobs-synthetics/static-url-hash' is forbidden"}, + }, + }, + { + name: "returns 403 when trying to update status to non-deleted value with labels", + probeID: probeID, + reqBody: v1.UpdateProbeJSONRequestBody{ + Status: &newStatus, + Labels: &v1.LabelsSchema{"environment": "prod"}, + }, + store: &mockProbeStore{ + probes: map[uuid.UUID]v1.ProbeObject{probeID: initialProbe}, + }, + expectedResponse: v1.UpdateProbe403JSONResponse{ + Error: v1.ErrorObject{Message: "modification of status field is forbidden - it's managed by the system (only deletion via 'deleted' status is allowed)"}, + }, + }, } for _, tc := range testCases { diff --git a/pkg/apis/v1/types.go b/pkg/apis/v1/types.go index 9b94086..79a0db6 100644 --- a/pkg/apis/v1/types.go +++ b/pkg/apis/v1/types.go @@ -87,6 +87,9 @@ type StatusSchema string // UpdateProbeRequest Fields to update for a probe. type UpdateProbeRequest struct { + // Labels A set of key-value pairs that can be used to organize and select probes. + Labels *LabelsSchema `json:"labels,omitempty"` + // Status The current status of the probe. Status *StatusSchema `json:"status,omitempty"` } @@ -532,6 +535,15 @@ func (response UpdateProbe400JSONResponse) VisitUpdateProbeResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type UpdateProbe403JSONResponse ErrorResponse + +func (response UpdateProbe403JSONResponse) VisitUpdateProbeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type UpdateProbe404JSONResponse WarningResponse func (response UpdateProbe404JSONResponse) VisitUpdateProbeResponse(w http.ResponseWriter) error { @@ -734,30 +746,31 @@ func (sh *strictHandler) UpdateProbe(w http.ResponseWriter, r *http.Request, pro // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "1/Hyqvw7AAD//3gp4w5/FwAA", } // GetSwagger returns the content of the embedded swagger specification file From a130d34809bf9c0b0bce1a29caee07bab3d6aed5 Mon Sep 17 00:00:00 2001 From: erosente Date: Thu, 9 Oct 2025 16:46:02 -0400 Subject: [PATCH 2/2] Removed the validateprotected fields function --- internal/api/server.go | 29 ++++++----------------------- internal/api/server_test.go | 17 +++++++++++------ 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/internal/api/server.go b/internal/api/server.go index 078bcc5..1677509 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -42,8 +42,8 @@ func validateProtectedLabels(labels *v1.LabelsSchema) error { } protectedLabels := []string{ - baseAppLabelKey, // "app" - probeStatusLabelKey, // "rhobs-synthetics/status" + baseAppLabelKey, // "app" + // probeStatusLabelKey, // "rhobs-synthetics/status" - removed protection probeURLHashLabelKey, // "rhobs-synthetics/static-url-hash" } @@ -56,24 +56,6 @@ func validateProtectedLabels(labels *v1.LabelsSchema) error { return nil } -// validateProtectedFields checks if the user is trying to modify protected fields/labels -func validateProtectedFields(request *v1.UpdateProbeJSONRequestBody) error { - // Check if user is trying to modify protected labels - if err := validateProtectedLabels(request.Labels); err != nil { - return err - } - - // Check if user is trying to modify status (except for deletion) - if request.Status != nil { - // Only allow status change to "deleted" for deletion functionality - if *request.Status != v1.Deleted { - return fmt.Errorf("modification of status field is forbidden - it's managed by the system (only deletion via 'deleted' status is allowed)") - } - } - - return nil -} - // (GET /probes) func (s Server) ListProbes(ctx context.Context, request v1.ListProbesRequestObject) (v1.ListProbesResponseObject, error) { defer metrics.RecordProbestoreRequest("list_probes", time.Now()) @@ -173,8 +155,9 @@ func (s Server) CreateProbe(ctx context.Context, request v1.CreateProbeRequestOb func (s Server) UpdateProbe(ctx context.Context, request v1.UpdateProbeRequestObject) (v1.UpdateProbeResponseObject, error) { defer metrics.RecordProbestoreRequest("update_probe", time.Now()) - // Validate that protected fields/labels are not being modified - return 403 if they are - if err := validateProtectedFields(request.Body); err != nil { + // Validate that protected labels are not being modified - return 403 if they are + // Note: Status field modifications are allowed (RMO can set terminating, agents can set active/failed) + if err := validateProtectedLabels(request.Body.Labels); err != nil { return v1.UpdateProbe403JSONResponse{ Error: v1.ErrorObject{ Message: err.Error(), @@ -197,7 +180,7 @@ func (s Server) UpdateProbe(ctx context.Context, request v1.UpdateProbeRequestOb return nil, fmt.Errorf("failed to get probe from storage for update: %w", err) } - // Handle status updates (only "deleted" is allowed, validated above) + // Now, update the fields from the request. if request.Body.Status != nil { existingProbe.Status = *request.Body.Status diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 12797b7..1e5f57d 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -377,14 +377,16 @@ func TestUpdateProbe(t *testing.T) { postCheck func(t *testing.T, store probestore.ProbeStorage) }{ { - name: "returns 403 when trying to update status field to non-deleted value", + name: "allows status field updates (RMO can set terminating, agents can set active/failed)", probeID: probeID, reqBody: v1.UpdateProbeJSONRequestBody{Status: &newStatus}, store: &mockProbeStore{ probes: map[uuid.UUID]v1.ProbeObject{probeID: initialProbe}, }, - expectedResponse: v1.UpdateProbe403JSONResponse{ - Error: v1.ErrorObject{Message: "modification of status field is forbidden - it's managed by the system (only deletion via 'deleted' status is allowed)"}, + expectedResponse: v1.UpdateProbe200JSONResponse{ + Id: probeID, + StaticUrl: "https://example.com", + Status: newStatus, }, }, { @@ -492,7 +494,7 @@ func TestUpdateProbe(t *testing.T) { }, }, { - name: "returns 403 when trying to update status to non-deleted value with labels", + name: "allows status updates with labels (RMO can set terminating, agents can set active/failed)", probeID: probeID, reqBody: v1.UpdateProbeJSONRequestBody{ Status: &newStatus, @@ -501,8 +503,11 @@ func TestUpdateProbe(t *testing.T) { store: &mockProbeStore{ probes: map[uuid.UUID]v1.ProbeObject{probeID: initialProbe}, }, - expectedResponse: v1.UpdateProbe403JSONResponse{ - Error: v1.ErrorObject{Message: "modification of status field is forbidden - it's managed by the system (only deletion via 'deleted' status is allowed)"}, + expectedResponse: v1.UpdateProbe200JSONResponse{ + Id: probeID, + StaticUrl: "https://example.com", + Status: newStatus, + Labels: &v1.LabelsSchema{"environment": "prod"}, }, }, }