diff options
author | John Cai <jcai@gitlab.com> | 2020-05-10 06:44:47 +0300 |
---|---|---|
committer | John Cai <jcai@gitlab.com> | 2020-05-15 18:49:59 +0300 |
commit | 9738045a3d0cc8f453fb68716602c01dbb2c41be (patch) | |
tree | 23b221b2ab3ddc089018c3e16eade9f6908efefe | |
parent | 99aafef6e94851fe6b18e44ebbfd642106d0d09d (diff) |
Add gitlab api access
-rw-r--r-- | NOTICE | 28 | ||||
-rw-r--r-- | go.mod | 3 | ||||
-rw-r--r-- | go.sum | 41 | ||||
-rw-r--r-- | internal/service/hooks/api/access.go | 208 | ||||
-rw-r--r-- | internal/service/hooks/api/access_test.go | 288 | ||||
-rw-r--r-- | internal/testhelper/testserver.go | 115 |
6 files changed, 652 insertions, 31 deletions
@@ -2264,6 +2264,34 @@ LICENSE.txt - gitlab.com/gitlab-org/gitaly/internal/praefect/grpc-proxy/proxy incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LICENSE - gitlab.com/gitlab-org/gitlab-shell/client +Copyright (c) 2011-2018 GitLab B.V. + +With regard to the GitLab Software: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +For all third party components incorporated into the GitLab Software, those +components are licensed under the original license provided by the owner of the +applicable component. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LICENSE - gitlab.com/gitlab-org/labkit The MIT License (MIT) @@ -12,12 +12,13 @@ require ( github.com/kelseyhightower/envconfig v1.3.0 github.com/lib/pq v1.2.0 github.com/olekukonko/tablewriter v0.0.2 + github.com/otiai10/curr v1.0.0 // indirect github.com/prometheus/client_golang v1.0.0 github.com/prometheus/procfs v0.0.3 // indirect github.com/rubenv/sql-migrate v0.0.0-20191213152630-06338513c237 github.com/sirupsen/logrus v1.4.2 github.com/stretchr/testify v1.4.0 - github.com/tinylib/msgp v1.1.0 // indirect + gitlab.com/gitlab-org/gitlab-shell v1.9.8-0.20200506213341-716e30c55e89 gitlab.com/gitlab-org/labkit v0.0.0-20200507062444-0149780c759d golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e golang.org/x/sys v0.0.0-20200113162924-86b910548bc1 @@ -1,3 +1,5 @@ +bou.ke/monkey v1.0.1 h1:zEMLInw9xvNakzUUPjfS4Ds6jYPqCFx3m7bRmG5NH2U= +bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg= cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -74,6 +76,7 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/getsentry/raven-go v0.1.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/getsentry/raven-go v0.1.2/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/getsentry/sentry-go v0.5.1 h1:MIPe7ScHADsrK2vznqmhksIUFxq7m0JfTh+ZIMkI+VQ= github.com/getsentry/sentry-go v0.5.1/go.mod h1:B8H7x8TYDPkeWPRzGpIiFO97LZP6rL8A3hEt8lUItMw= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= @@ -108,6 +111,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= @@ -194,6 +198,7 @@ github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvf github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/libgit2/git2go v0.0.0-20190104134018-ecaeb7a21d47/go.mod h1:4bKN42efkbNYMZlvDfxGDxzl066GhpvIircZDsm8Y+Y= github.com/lightstep/lightstep-tracer-go v0.15.6 h1:D0GGa7afJ7GcQvu5as6ssLEEKYXvRgKI5d5cevtz8r4= github.com/lightstep/lightstep-tracer-go v0.15.6/go.mod h1:6AMpwZpsyCFwSovxzM78e+AsYxE8sGwiM6C3TytaWeI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -205,6 +210,7 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-shellwords v0.0.0-20190425161501-2444a32a19f4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-sqlite3 v1.12.0 h1:u/x3mp++qUxvYfulZ4HKOvVO0JWhk7HtE8lWhbGz/Do= github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= @@ -227,12 +233,23 @@ github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXW github.com/olekukonko/tablewriter v0.0.2 h1:sq53g+DWf0J6/ceFUHpQ0nAEb6WgM++fq16MZ91cS6o= github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/otiai10/copy v1.0.1 h1:gtBjD8aq4nychvRZ2CyJvFWAw0aja+VHazDdruZKGZA= +github.com/otiai10/copy v1.0.1/go.mod h1:8bMCJrAqOtN/d9oyh5HR7HhLQMvcGMpGdwRDYsfOCHc= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.2.3 h1:PsrRBmrxR68kyNu6YlqYHbNlItc5vOkuS6LBEsNttVA= +github.com/otiai10/mint v1.2.3/go.mod h1:YnfyPNhBvnY8bW4SGQHCs/aAFhkgySlMZbrF5U0bOVw= +github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= @@ -327,6 +344,10 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +gitlab.com/gitlab-org/gitaly v1.68.0/go.mod h1:/pCsB918Zu5wFchZ9hLYin9WkJ2yQqdVNz0zlv5HbXg= +gitlab.com/gitlab-org/gitlab-shell v1.9.8-0.20200506213341-716e30c55e89 h1:gZwXV5WLPmJ3NDmH+zAZkgWLVhgKvamGAjLXU5W0GiU= +gitlab.com/gitlab-org/gitlab-shell v1.9.8-0.20200506213341-716e30c55e89/go.mod h1:oUGdKtJHWQP4/VQ/NONJZy/8X2E5ow2eGTUDwdzvGsc= +gitlab.com/gitlab-org/labkit v0.0.0-20190221122536-0c3fc7cdd57c/go.mod h1:rYhLgfrbEcyfinG+R3EvKu6bZSsmwQqcXzLfHWSfUKM= gitlab.com/gitlab-org/labkit v0.0.0-20200507062444-0149780c759d h1:Q5yZi+AelheHuvq/OK6DiaBzLU1AHrm7eWh88uE8Tsk= gitlab.com/gitlab-org/labkit v0.0.0-20200507062444-0149780c759d/go.mod h1:SNfxkfUwVNECgtmluVayv0GWFgEjjBs5AzgsowPQuo0= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -341,6 +362,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90Pveol golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= @@ -356,6 +378,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299 h1:zQpM52jfKHG6II1ISZY1Zcpyg golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -372,6 +395,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -382,25 +406,21 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -410,7 +430,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -423,18 +442,17 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1 h1:gZpLHxUX5BdYLA08Lj4YCJNN/jk7KtquiArPoeX0WvA= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -469,7 +487,6 @@ google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEn google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -477,6 +494,7 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -487,6 +505,7 @@ google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBr google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba h1:pRj9OXZbwNtbtZtOB4dLwfK4u+EVRMvP+e9zKkg2grM= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -495,7 +514,6 @@ google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRn gopkg.in/DataDog/dd-trace-go.v1 v1.7.0 h1:7wbMayb6JXcbAS95RN7MI42W3o1BCxCcdIzZfVWBAiE= gopkg.in/DataDog/dd-trace-go.v1 v1.7.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -510,13 +528,12 @@ gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3M gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 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.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/service/hooks/api/access.go b/internal/service/hooks/api/access.go new file mode 100644 index 000000000..dbf2b7c58 --- /dev/null +++ b/internal/service/hooks/api/access.go @@ -0,0 +1,208 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime" + "net/http" + "path/filepath" + "regexp" + "strings" + + "gitlab.com/gitlab-org/gitaly/internal/helper" + "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb" + "gitlab.com/gitlab-org/gitlab-shell/client" +) + +// AllowedResponse is a response for the internal gitlab api's /allowed endpoint with a subset +// of fields +type AllowedResponse struct { + Status bool `json:"status"` + Message string `json:"message"` +} + +// AllowedRequest is a request for the internal gitlab api /allowed endpoint +type AllowedRequest struct { + Action string `json:"action,omitempty"` + GLRepository string `json:"gl_repository,omitempty"` + Project string `json:"project,omitempty"` + Changes string `json:"changes,omitempty"` + Protocol string `json:"protocol,omitempty"` + Env string `json:"env,omitempty"` + Username string `json:"username,omitempty"` + KeyID string `json:"key_id,omitempty"` + UserID string `json:"user_id,omitempty"` +} + +// gitObjectDirs generates a json encoded string containing GIT_OBJECT_DIRECTORY_RELATIVE, and GIT_ALTERNATE_OBJECT_DIRECTORIES +func gitObjectDirs(repoPath, gitObjectDir string, gitAltObjDirs []string) (string, error) { + gitObjDirRel, err := filepath.Rel(repoPath, gitObjectDir) + if err != nil { + return "", err + } + + gitAltObjDirsRel, err := relativeAlternativeObjectPaths(repoPath, gitAltObjDirs) + if err != nil { + return "", err + } + + envString, err := json.Marshal(map[string]interface{}{ + "GIT_OBJECT_DIRECTORY_RELATIVE": gitObjDirRel, + "GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE": gitAltObjDirsRel, + }) + + if err != nil { + return "", err + } + + return string(envString), nil +} + +func relativeAlternativeObjectPaths(repoPath string, gitAltObjDirs []string) ([]string, error) { + relPaths := make([]string, 0, len(gitAltObjDirs)) + for _, objPath := range gitAltObjDirs { + relPath, err := filepath.Rel(repoPath, objPath) + if err != nil { + return relPaths, err + } + relPaths = append(relPaths, relPath) + } + + return relPaths, nil +} + +// API is a wrapper around client.GitlabNetClient with api methods for gitlab git receive hooks +type API struct { + client *client.GitlabNetClient +} + +// New creates a new API +func New(c *client.GitlabNetClient) *API { + return &API{ + client: c, + } +} + +// Allowed checks if a ref change for a given repository is allowed through the gitlab internal api /allowed endpoint +func (a *API) Allowed(repo *gitalypb.Repository, glRepository, glID, glProtocol, changes string) (bool, error) { + repoPath, err := helper.GetRepoPath(repo) + if err != nil { + return false, fmt.Errorf("getting the repository path: %w", err) + } + + gitObjDirVars, err := gitObjectDirs(repoPath, repo.GetGitObjectDirectory(), repo.GetGitAlternateObjectDirectories()) + if err != nil { + return false, fmt.Errorf("when getting git object directories json encoded string: %w", err) + } + + req := AllowedRequest{ + Action: "git-receive-pack", + GLRepository: glRepository, + Changes: changes, + Protocol: glProtocol, + Project: strings.Replace(repoPath, "'", "", -1), + Env: gitObjDirVars, + } + + if err := req.parseAndSetGLID(glID); err != nil { + return false, fmt.Errorf("setting gl_id: %w", err) + } + + resp, err := a.client.Post("/allowed", &req) + if err != nil { + return false, fmt.Errorf("http post to gitlab api /allowed endpoint: %w", err) + } + + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() + + var response AllowedResponse + + switch resp.StatusCode { + case http.StatusOK, + http.StatusMultipleChoices: + + mtype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return false, fmt.Errorf("/allowed endpoint respond with unsupported content type: %w", err) + } + + if mtype != "application/json" { + return false, fmt.Errorf("/allowed endpoint respond with unsupported content type: %s", mtype) + } + + if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { + return false, fmt.Errorf("decoding response from /allowed endpoint: %w", err) + } + default: + return false, fmt.Errorf("API is not accessible: %d", resp.StatusCode) + } + + return response.Status, nil +} + +type preReceiveResponse struct { + ReferenceCounterIncreased bool `json:"reference_counter_increased"` +} + +// PreReceive increases the reference counter for a push for a given gl_repository through the gitlab internal api /pre_receive endpoint +func (a *API) PreReceive(glRepository string) (bool, error) { + resp, err := a.client.Post("/pre_receive", map[string]string{"gl_repository": glRepository}) + if err != nil { + return false, fmt.Errorf("http post to gitlab api /pre_receive endpoint: %w", err) + } + + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("pre-receive call failed with status: %d", resp.StatusCode) + } + + mtype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return false, fmt.Errorf("/pre_receive endpoint respond with unsupported content type: %w", err) + } + + if mtype != "application/json" { + return false, fmt.Errorf("/pre_receive endpoint respond with unsupported content type: %s", mtype) + } + + var result preReceiveResponse + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return false, fmt.Errorf("decoding response from /pre_receive endpoint: %w", err) + } + + return result.ReferenceCounterIncreased, nil +} + +var glIDRegex = regexp.MustCompile(`\A[0-9]+\z`) + +func (a *AllowedRequest) parseAndSetGLID(glID string) error { + var value string + + switch { + case strings.HasPrefix(glID, "username-"): + a.Username = strings.TrimPrefix(glID, "username-") + return nil + case strings.HasPrefix(glID, "key-"): + a.KeyID = strings.TrimPrefix(glID, "key-") + value = a.KeyID + case strings.HasPrefix(glID, "user-"): + a.UserID = strings.TrimPrefix(glID, "user-") + value = a.UserID + } + + if !glIDRegex.MatchString(value) { + return fmt.Errorf("gl_id='%s' is invalid!", glID) + } + + return nil +} diff --git a/internal/service/hooks/api/access_test.go b/internal/service/hooks/api/access_test.go new file mode 100644 index 000000000..948ccfb90 --- /dev/null +++ b/internal/service/hooks/api/access_test.go @@ -0,0 +1,288 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/internal/service/hooks/api" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" + "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb" + "gitlab.com/gitlab-org/gitlab-shell/client" +) + +func TestMain(m *testing.M) { + testhelper.Configure() + os.Exit(m.Run()) +} + +func TestAllowedVerifyParams(t *testing.T) { + user, password := "user", "password" + secretToken := "topsecret" + glID, glRepository := "key-123", "repo-1" + + testRepo, testRepoPath, cleanup := testhelper.NewTestRepo(t) + defer cleanup() + changes := "changes1\nchanges2\nchanges3" + protocol := "protocol" + gitObjectDir := filepath.Join(testRepoPath, "object/dir") + gitAlternateObjectDirs := []string{filepath.Join(testRepoPath, "alt/object/dir1"), filepath.Join(testRepoPath, "alt/object/dir1")} + + testRepo.GitObjectDirectory = gitObjectDir + testRepo.GitAlternateObjectDirectories = gitAlternateObjectDirs + + server := testhelper.NewGitlabTestServer(t, testhelper.GitlabTestServerOptions{ + User: user, + Password: password, + SecretToken: secretToken, + GLID: glID, + GLRepository: glRepository, + Changes: changes, + PostReceiveCounterDecreased: true, + Protocol: protocol, + GitPushOptions: nil, + GitObjectDir: gitObjectDir, + GitAlternateObjectDirs: gitAlternateObjectDirs, + RepoPath: testRepoPath, + }) + + defer server.Close() + + httpClient := client.NewHTTPClient(server.URL, "", "", false, 100) + gitlabnetClient, err := client.NewGitlabNetClient(user, password, secretToken, httpClient) + require.NoError(t, err) + + c := api.New(gitlabnetClient) + + badRepo := *testRepo + badRepo.GitObjectDirectory = filepath.Join(testRepoPath, "bad/object/directory") + + testCases := []struct { + desc string + repo *gitalypb.Repository + glRepository, glID, protocol, changes string + allowed bool + }{ + { + desc: "success", + repo: testRepo, + glRepository: glRepository, + glID: glID, + protocol: protocol, + changes: changes, + allowed: true, + }, + { + desc: "repo with bad quarantine directories", + repo: &badRepo, + glRepository: glRepository, + glID: glID, + protocol: protocol, + changes: changes, + allowed: false, + }, + } + + for _, tc := range testCases { + allowed, err := c.Allowed(tc.repo, tc.glRepository, tc.glID, tc.protocol, tc.changes) + require.NoError(t, err) + require.Equal(t, tc.allowed, allowed) + } +} + +func TestAllowedResponseHandling(t *testing.T) { + testRepo, testRepoPath, cleanup := testhelper.NewTestRepo(t) + + // set git quarantine directories + gitObjectDir := filepath.Join(testRepoPath, "quarantine", "object", "dir") + testRepo.GitObjectDirectory = gitObjectDir + gitAltObjectDir := filepath.Join(testRepoPath, "objects") + testRepo.GitAlternateObjectDirectories = []string{gitAltObjectDir} + + defer cleanup() + + testCases := []struct { + desc string + allowedHandler func(w http.ResponseWriter, r *http.Request) + allowed bool + errMsg string + }{ + { + desc: "allowed", + allowedHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status": true}`)) + }, + allowed: true, + errMsg: "", + }, + { + desc: "bad content type in response", + allowedHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "bad mime type") + w.WriteHeader(http.StatusOK) + }, + allowed: false, + errMsg: "unsupported content type", + }, + { + desc: "internal server error", + allowedHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"status": true}`)) + }, + allowed: false, + errMsg: "", + }, + { + desc: "bad response", + allowedHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`this is not json`)) + }, + allowed: false, + errMsg: "decoding response from /allowed endpoint", + }, + { + desc: "status multiple choice", + allowedHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusMultipleChoices) + w.Write([]byte(`{"status": true}`)) + }, + allowed: true, + errMsg: "", + }, + { + desc: "status unauthorized with message", + allowedHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"message": "you're not allowed here'"}`)) + }, + allowed: false, + errMsg: "you're not allowed here", + }, + { + desc: "status unauthorized", + allowedHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + }, + allowed: false, + errMsg: "Internal API error", + }, + { + desc: "status not found", + allowedHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"message": "not found"}`)) + }, + allowed: false, + errMsg: "not found", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(tc.allowedHandler)) + defer server.Close() + + httpClient := client.NewHTTPClient(server.URL, "", "", false, 100) + gitlabnetClient, err := client.NewGitlabNetClient("", "", "", httpClient) + require.NoError(t, err) + + c := api.New(gitlabnetClient) + allowed, err := c.Allowed(testRepo, "repo-1", "key-123", "http", "a\nb\nc\nd") + require.Equal(t, tc.allowed, allowed) + if err != nil { + require.Contains(t, err.Error(), tc.errMsg) + } + }) + } +} + +func TestPrereceive(t *testing.T) { + testCases := []struct { + desc string + prereceiveHandler func(w http.ResponseWriter, r *http.Request) + success bool + errMsg string + }{ + { + desc: "everything ok", + prereceiveHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"reference_counter_increased": true}`)) + }, + success: true, + errMsg: "", + }, + { + desc: "reference counter not increased", + prereceiveHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"reference_counter_increased": false}`)) + }, + success: false, + errMsg: "", + }, + { + desc: "server unavailable", + prereceiveHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte(`{"message": "server is down!"}`)) + }, + success: false, + errMsg: "server is down!", + }, + { + desc: "non json content type", + prereceiveHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"reference_counter_increased": true}`)) + }, + success: false, + errMsg: "unsupported content type", + }, + { + desc: "bad data", + prereceiveHandler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`not json`)) + }, + success: false, + errMsg: "decoding response from /pre_receive endpoint", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(tc.prereceiveHandler)) + defer server.Close() + + httpClient := client.NewHTTPClient(server.URL, "", "", false, 100) + gitlabnetClient, err := client.NewGitlabNetClient("", "", "", httpClient) + require.NoError(t, err) + + c := api.New(gitlabnetClient) + success, err := c.PreReceive("key-123") + require.Equal(t, tc.success, success) + if err != nil { + require.Contains(t, err.Error(), tc.errMsg) + } + }) + } +} diff --git a/internal/testhelper/testserver.go b/internal/testhelper/testserver.go index 27c160ca6..8d259c995 100644 --- a/internal/testhelper/testserver.go +++ b/internal/testhelper/testserver.go @@ -2,6 +2,7 @@ package testhelper import ( "context" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -9,6 +10,7 @@ import ( "net" "net/http" "net/http/httptest" + "net/url" "os" "os/exec" "path/filepath" @@ -277,11 +279,35 @@ func NewServer(tb testing.TB, streamInterceptors []grpc.StreamServerInterceptor, var changeLineRegex = regexp.MustCompile("^[a-f0-9]{40} [a-f0-9]{40} refs/[^ ]+$") +const secretHeaderName = "Gitlab-Shared-Secret" + +func formToMap(u url.Values) map[string]string { + return map[string]string{ + "action": u.Get("action"), + "gl_repository": u.Get("gl_repository"), + "project": u.Get("project"), + "changes": u.Get("changes"), + "protocol": u.Get("protocol"), + "env": u.Get("env"), + "username": u.Get("username"), + "key_id": u.Get("key_id"), + "user_id": u.Get("user_id"), + } +} + func handleAllowed(t testing.TB, options GitlabTestServerOptions) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { require.NoError(t, r.ParseForm()) require.Equal(t, http.MethodPost, r.Method, "expected http post") - require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) + + params := make(map[string]string) + + switch r.Header.Get("Content-Type") { + case "application/x-www-form-urlencoded": + params = formToMap(r.Form) + case "application/json": + require.NoError(t, json.NewDecoder(r.Body).Decode(¶ms)) + } user, password, _ := r.BasicAuth() require.Equal(t, options.User, user) @@ -293,58 +319,79 @@ func handleAllowed(t testing.TB, options GitlabTestServerOptions) func(w http.Re switch glidSplit[0] { case "user": - require.Equal(t, glidSplit[1], r.Form.Get("user_id")) + require.Equal(t, glidSplit[1], params["user_id"]) case "key": - require.Equal(t, glidSplit[1], r.Form.Get("key_id")) + require.Equal(t, glidSplit[1], params["key_id"]) case "username": - require.Equal(t, glidSplit[1], r.Form.Get("username")) + require.Equal(t, glidSplit[1], params["username"]) default: t.Fatalf("invalid GLID: %q", options.GLID) } } - require.NotEmpty(t, r.Form.Get("gl_repository"), "gl_repository should not be empty") + require.NotEmpty(t, params["gl_repository"], "gl_repository should not be empty") if options.GLRepository != "" { - require.Equal(t, options.GLRepository, r.Form.Get("gl_repository"), "expected value of gl_repository should match form") + require.Equal(t, options.GLRepository, params["gl_repository"], "expected value of gl_repository should match form") } - require.NotEmpty(t, r.Form.Get("protocol"), "protocol should not be empty") + require.NotEmpty(t, params["protocol"], "protocol should not be empty") if options.Protocol != "" { - require.Equal(t, options.Protocol, r.Form.Get("protocol"), "expected value of options.Protocol should match form") + require.Equal(t, options.Protocol, params["protocol"], "expected value of options.Protocol should match form") } if options.Changes != "" { - require.Equal(t, options.Changes, r.Form.Get("changes"), "expected value of options.Changes should match form") + require.Equal(t, options.Changes, params["changes"], "expected value of options.Changes should match form") } else { - changeLines := strings.Split(strings.TrimSuffix(r.Form.Get("changes"), "\n"), "\n") + changeLines := strings.Split(strings.TrimSuffix(params["changes"], "\n"), "\n") for _, line := range changeLines { require.Regexp(t, changeLineRegex, line) } } - env := r.Form.Get("env") + env := params["env"] require.NotEmpty(t, env) var gitVars struct { GitAlternateObjectDirsRel []string `json:"GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE"` GitObjectDirRel string `json:"GIT_OBJECT_DIRECTORY_RELATIVE"` } + + w.Header().Set("Content-Type", "application/json") + require.NoError(t, json.Unmarshal([]byte(env), &gitVars)) if options.GitObjectDir != "" { relObjectDir, err := filepath.Rel(options.RepoPath, options.GitObjectDir) require.NoError(t, err) - require.Equal(t, relObjectDir, gitVars.GitObjectDirRel) + if relObjectDir != gitVars.GitObjectDirRel { + w.Write([]byte(`{"status":false}`)) + return + } } + if len(options.GitAlternateObjectDirs) > 0 { require.Len(t, gitVars.GitAlternateObjectDirsRel, len(options.GitAlternateObjectDirs)) for i, gitAlterateObjectDir := range options.GitAlternateObjectDirs { relAltObjectDir, err := filepath.Rel(options.RepoPath, gitAlterateObjectDir) require.NoError(t, err) - require.Equal(t, relAltObjectDir, gitVars.GitAlternateObjectDirsRel[i]) + if relAltObjectDir != gitVars.GitAlternateObjectDirsRel[i] { + w.Write([]byte(`{"status":false}`)) + return + } } } - w.Header().Set("Content-Type", "application/json") + var authenticated bool if r.Form.Get("secret_token") == options.SecretToken { + authenticated = true + } + + secretHeader, err := base64.StdEncoding.DecodeString(r.Header.Get(secretHeaderName)) + if err == nil { + if string(secretHeader) == options.SecretToken { + authenticated = true + } + } + + if authenticated { w.Write([]byte(`{"status":true}`)) return } @@ -356,13 +403,44 @@ func handleAllowed(t testing.TB, options GitlabTestServerOptions) func(w http.Re func handlePreReceive(t testing.TB, options GitlabTestServerOptions) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { require.NoError(t, r.ParseForm()) + + params := make(map[string]string) + + switch r.Header.Get("Content-Type") { + case "application/x-www-form-urlencoded": + b, err := json.Marshal(r.Form) + require.NoError(t, err) + + var reqForm struct { + GLRepository []string `json:"gl_repository"` + } + + require.NoError(t, json.Unmarshal(b, &reqForm)) + require.Greater(t, len(reqForm.GLRepository), 0) + params["gl_repository"] = reqForm.GLRepository[0] + case "application/json": + require.NoError(t, json.NewDecoder(r.Body).Decode(¶ms)) + } + require.Equal(t, http.MethodPost, r.Method) - require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) - require.NotEmpty(t, r.Form.Get("gl_repository"), "gl_repository should not be empty") + require.NotEmpty(t, params["gl_repository"], "gl_repository should not be empty") if options.GLRepository != "" { - require.Equal(t, options.GLRepository, r.Form.Get("gl_repository"), "expected value of gl_repository should match form") + require.Equal(t, options.GLRepository, params["gl_repository"], "expected value of gl_repository should match form") + } + + var authenticated bool + if r.Form.Get("secret_token") == options.SecretToken { + authenticated = true } - require.Equal(t, options.SecretToken, r.Form.Get("secret_token"), "expected value of secret_token should match form") + + secretHeader, err := base64.StdEncoding.DecodeString(r.Header.Get(secretHeaderName)) + if err == nil { + if string(secretHeader) == options.SecretToken { + authenticated = true + } + } + + require.True(t, authenticated, "expected value of secret_token should request") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -372,6 +450,7 @@ func handlePreReceive(t testing.TB, options GitlabTestServerOptions) func(w http func handlePostReceive(t testing.TB, options GitlabTestServerOptions) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() require.NoError(t, r.ParseForm()) require.Equal(t, http.MethodPost, r.Method) require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) |