diff --git a/Dockerfile b/Dockerfile index 363ed5c..d34999c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.16 as builder +FROM golang:1.17 as builder RUN mkdir /build ADD . /build/ WORKDIR /build diff --git a/go.mod b/go.mod index 0b4e4b4..33bb062 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,10 @@ module github.com/xenitab/azdo-proxy -go 1.16 +go 1.17 require ( + github.com/bradleyfalzon/ghinstallation/v2 v2.0.3 + github.com/cristalhq/aconfig v0.16.6 github.com/go-logr/logr v0.4.0 github.com/go-logr/zapr v0.4.0 github.com/go-playground/validator/v10 v10.9.0 @@ -14,13 +16,57 @@ require ( github.com/prometheus/client_golang v1.11.0 github.com/slok/go-http-metrics v0.9.0 github.com/spf13/afero v1.6.0 - github.com/spf13/pflag v1.0.5 github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.7.0 go.uber.org/zap v1.18.1 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - google.golang.org/appengine v1.6.6 // indirect - k8s.io/api v0.22.1 - k8s.io/apimachinery v0.22.1 - k8s.io/client-go v0.22.1 + k8s.io/api v0.22.2 + k8s.io/apimachinery v0.22.2 + k8s.io/client-go v0.22.2 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/evanphx/json-patch v4.11.0+incompatible // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.0.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.6 // indirect + github.com/google/go-github/v39 v39.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/googleapis/gnostic v0.5.5 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.26.0 // indirect + github.com/prometheus/procfs v0.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 // indirect + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect + golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect + golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + k8s.io/klog/v2 v2.9.0 // indirect + k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect + k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect ) diff --git a/go.sum b/go.sum index 103ccd2..c954c1b 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradleyfalzon/ghinstallation/v2 v2.0.3 h1:ywF/8q+GVpvlsEuvRb1SGSDQDUxntW1d4kFu/9q/YAE= +github.com/bradleyfalzon/ghinstallation/v2 v2.0.3/go.mod h1:tlgi+JWCXnKFx/Y4WtnDbZEINo31N5bcvnCoqieefmk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= @@ -55,6 +57,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cristalhq/aconfig v0.16.6 h1:SnwVPXhJldbu8bFos1ru29Xmxi3yZRXkSL6SiHJmGFQ= +github.com/cristalhq/aconfig v0.16.6/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E= 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= @@ -107,6 +111,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -141,9 +147,15 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a 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.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github/v39 v39.0.0 h1:pygGA5ySwxEez1N39GnDauD0PaWWuGgayudyZAc941s= +github.com/google/go-github/v39 v39.0.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -333,8 +345,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -527,8 +540,8 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/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-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -599,20 +612,20 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.22.1 h1:ISu3tD/jRhYfSW8jI/Q1e+lRxkR7w9UwQEZ7FgslrwY= -k8s.io/api v0.22.1/go.mod h1:bh13rkTp3F1XEaLGykbyRD2QaTTzPm0e/BMd8ptFONY= -k8s.io/apimachinery v0.22.1 h1:DTARnyzmdHMz7bFWFDDm22AM4pLWTQECMpRTFu2d2OM= -k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= -k8s.io/client-go v0.22.1 h1:jW0ZSHi8wW260FvcXHkIa0NLxFBQszTlhiAVsU5mopw= -k8s.io/client-go v0.22.1/go.mod h1:BquC5A4UOo4qVDUtoc04/+Nxp1MeHcVc1HJm1KmG8kk= +k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= +k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= +k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk= +k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= +k8s.io/client-go v0.22.2 h1:DaSQgs02aCC1QcwUdkKZWOeaVsQjYvWv8ZazcZ6JcHc= +k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9 h1:imL9YgXQ9p7xmPzHFm/vVd/cF78jad+n4wK1ABwYtMM= -k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main.go b/main.go index 7ed42c7..3b10e57 100644 --- a/main.go +++ b/main.go @@ -7,16 +7,16 @@ import ( "net/http" "os" "os/signal" - "path/filepath" "syscall" "time" "golang.org/x/sync/errgroup" + "github.com/cristalhq/aconfig" "github.com/go-logr/logr" "github.com/go-logr/zapr" "github.com/prometheus/client_golang/prometheus/promhttp" - flag "github.com/spf13/pflag" + "github.com/spf13/afero" "go.uber.org/zap" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -28,19 +28,11 @@ import ( "github.com/xenitab/azdo-proxy/pkg/token" ) -var ( - port string - metricsPort string - kubeconfigPath string - configPath string -) - -func init() { - flag.StringVar(&port, "port", ":8080", "port to bind proxy server to.") - flag.StringVar(&metricsPort, "metrics-port", ":9090", "port to bind metrics endpoint to.") - flag.StringVar(&kubeconfigPath, "kubeconfig", "", "absolute path to the kubeconfig file.") - flag.StringVar(&configPath, "config", "/var/config.json", "path to configuration file.") - flag.Parse() +type cfg struct { + Addr string `flag:"addr" default:":8080"` + MetricsAddr string `flag:"metrics-addr" default:":9090"` + KubeconfigPath string `flag:"kubeconfig"` + ConfigPath string `flag:"config"` } func main() { @@ -49,22 +41,33 @@ func main() { panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) } logger := zapr.NewLogger(zapLog) - if err := run(logger, configPath, metricsPort); err != nil { + if err := run(logger); err != nil { logger.WithName("main").Error(err, "run error") os.Exit(1) } } -func run(logger logr.Logger, configPath string, metricsPort string) error { +// nolint:funlen // cant make this shorter +func run(logger logr.Logger) error { mainLogger := logger.WithName("main") - mainLogger.Info("read configuration from", "path", configPath) - mainLogger.Info("serve metrics", "port", metricsPort, "endpoint", "/metrics") - authz, err := getAutorization(configPath) + cfg := &cfg{} + loader := aconfig.LoaderFor(cfg, aconfig.Config{ + FlagDelimiter: "-", + AllFieldRequired: false, + }) + if err := loader.Load(); err != nil { + return err + } + + mainLogger.Info("read configuration from", "path", cfg.ConfigPath) + mainLogger.Info("serve metrics", "port", cfg.MetricsAddr, "endpoint", "/metrics") + + authz, err := getAutorization(cfg.ConfigPath) if err != nil { return err } - client, err := getKubernetesClient(kubeconfigPath) + client, err := getKubernetesClient(cfg.KubeconfigPath) if err != nil { return fmt.Errorf("invalid kubernetes client: %w", err) } @@ -79,7 +82,7 @@ func run(logger logr.Logger, configPath string, metricsPort string) error { signal.Notify(stopCh, syscall.SIGTERM, syscall.SIGINT) defer signal.Stop(stopCh) - metricsSrv := &http.Server{Addr: metricsPort, Handler: promhttp.Handler()} + metricsSrv := &http.Server{Addr: cfg.MetricsAddr, Handler: promhttp.Handler()} g.Go(func() error { if err := metricsSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return err @@ -93,12 +96,9 @@ func run(logger logr.Logger, configPath string, metricsPort string) error { } return nil }) - azdoSrv, err := server.NewAzdoServer(logger, port, authz) - if err != nil { - return fmt.Errorf("could not create azdo proxy server: %w", err) - } + srv := server.NewServer(logger, cfg.Addr, authz) g.Go(func() error { - if err := azdoSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return err } return nil @@ -125,7 +125,7 @@ func run(logger logr.Logger, configPath string, metricsPort string) error { return nil }) g.Go(func() error { - if err := azdoSrv.Shutdown(timeoutCtx); err != nil { + if err := srv.Shutdown(timeoutCtx); err != nil { return err } return nil @@ -138,18 +138,14 @@ func run(logger logr.Logger, configPath string, metricsPort string) error { return nil } -func getAutorization(path string) (auth.Authorization, error) { - path, err := filepath.Rel("/", path) - if err != nil { - return auth.Authorization{}, fmt.Errorf("could not get relative path: %w", err) - } - cfg, err := config.LoadConfiguration(os.DirFS("/"), path) +func getAutorization(path string) (*auth.Authorizer, error) { + cfg, err := config.LoadConfiguration(afero.NewOsFs(), path) if err != nil { - return auth.Authorization{}, fmt.Errorf("could not load configuration: %w", err) + return nil, fmt.Errorf("could not load configuration: %w", err) } - authz, err := auth.NewAuthorization(cfg) + authz, err := auth.NewAuthorizer(cfg) if err != nil { - return auth.Authorization{}, fmt.Errorf("could not generate authorization: %w", err) + return nil, fmt.Errorf("could not generate authorization: %w", err) } return authz, nil } diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index deb6004..1f8fdce 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -1,120 +1,117 @@ package auth import ( - "crypto/rand" - "encoding/base64" - "errors" + "context" + b64 "encoding/base64" "fmt" + "net/http" "net/url" "regexp" "github.com/xenitab/azdo-proxy/pkg/config" ) -const tokenLenght = 64 - -type Authorization struct { - endpoints map[string]*Endpoint +type Provider interface { + getPathRegex(organization, project, repository string) ([]*regexp.Regexp, error) + getAuthorizationHeader(ctx context.Context, path string) (string, error) + getHost(e *Endpoint, path string) string + getPath(e *Endpoint, path string) string } -type Endpoint struct { - Pat string - Domain string - Scheme string - Token string - Namespaces []string - SecretName string - - // metadata used for reverse lookup - Organization string - Project string - Repository string - - regexes []*regexp.Regexp +type Authorizer struct { + providers map[string]Provider + endpoints []*Endpoint + endpointsByID map[string]*Endpoint + endpointsByToken map[string]*Endpoint } -// NewAuthorization cretes a new authorization from a give configuration. -func NewAuthorization(cfg *config.Configuration) (Authorization, error) { - authz := Authorization{endpoints: map[string]*Endpoint{}} +func NewAuthorizer(cfg *config.Configuration) (*Authorizer, error) { + providers := map[string]Provider{} + endpoints := []*Endpoint{} + endpointsByID := map[string]*Endpoint{} + endpointsByToken := map[string]*Endpoint{} + for _, o := range cfg.Organizations { - baseApi, err := regexp.Compile(fmt.Sprintf(`/%s/_apis\b`, o.Name)) - if err != nil { - return Authorization{}, fmt.Errorf("invalid base api regex: %w", err) + // Get the correct provider for the organization + var provider Provider + switch o.Provider { + case config.AzureDevOpsProviderType: + provider = newAzureDevops(o.AzureDevOps.Pat) + case config.GitHubProviderType: + pemData, err := b64.URLEncoding.DecodeString(o.GitHub.PrivateKey) + if err != nil { + return nil, err + } + provider, err = newGithub(o.GitHub.AppID, o.GitHub.InstallationID, pemData) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("invalid provider type %s", o.Provider) } + // Create endpoints for the repositories for _, r := range o.Repositories { - git, err := regexp.Compile(fmt.Sprintf(`/%s/%s/_git/%s(/.*)?\b`, o.Name, r.Project, r.Name)) + pathRegex, err := provider.getPathRegex(o.Name, r.Project, r.Name) if err != nil { - return Authorization{}, err - } - api, err := regexp.Compile(fmt.Sprintf(`/%s/%s/_apis/git/repositories/%s(/.*)?\b`, o.Name, r.Project, r.Name)) - if err != nil { - return Authorization{}, err + return nil, fmt.Errorf("could not get path regex: %w", err) } + token, err := randomSecureToken() if err != nil { - return Authorization{}, fmt.Errorf("could not generate random token: %w", err) + return nil, fmt.Errorf("could not generate random token: %w", err) } - e := Endpoint{ - Pat: o.Pat, - Domain: o.Domain, - Scheme: o.Scheme, + e := &Endpoint{ + host: o.Host, + scheme: o.Scheme, + organization: o.Name, + project: r.Project, + repository: r.Name, + regexes: pathRegex, Token: token, Namespaces: r.Namespaces, SecretName: o.GetSecretName(r), - Organization: o.Name, - Project: r.Project, - Repository: r.Name, - regexes: []*regexp.Regexp{baseApi, git, api}, } - authz.endpoints[token] = &e + + providers[e.ID()] = provider + endpoints = append(endpoints, e) + endpointsByID[e.ID()] = e + endpointsByToken[e.Token] = e } } + authz := &Authorizer{ + providers: providers, + endpoints: endpoints, + endpointsByID: endpointsByID, + endpointsByToken: endpointsByToken, + } return authz, nil } -// GetEndpoints returns all endpoints. -func (a *Authorization) GetEndpoints() map[string]*Endpoint { +func (a *Authorizer) GetEndpoints() []*Endpoint { return a.endpoints } -// LookupEndpoint returns the endpoint with the matching organization, project and repository. -func (a *Authorization) LookupEndpoint(domain, org, proj, repo string) (*Endpoint, error) { - for _, e := range a.endpoints { - if e.Domain == domain && e.Organization == org && e.Project == proj && e.Repository == repo { - return e, nil - } - } - return nil, errors.New("endpoint not found") -} - -// PatForToken returns the pat associated with the token. -func (a *Authorization) GetPatForToken(token string) (string, error) { - e, err := a.GetEndpointForToken(token) - if err != nil { - return "", err +func (a *Authorizer) GetEndpointById(id string) (*Endpoint, error) { + e, ok := a.endpointsByID[id] + if !ok { + return nil, fmt.Errorf("endpoint not found for id %s", id) } - return e.Pat, nil + return e, nil } -// TargetForToken returns the target url which matches the given token. -func (a *Authorization) GetTargetForToken(token string) (*url.URL, error) { - e, err := a.GetEndpointForToken(token) - if err != nil { - return nil, err - } - target, err := url.Parse(fmt.Sprintf("%s://%s", e.Scheme, e.Domain)) - if err != nil { - return nil, fmt.Errorf("invalid url format: %w", err) +func (a *Authorizer) GetEndpointByToken(token string) (*Endpoint, error) { + e, ok := a.endpointsByToken[token] + if !ok { + return nil, fmt.Errorf("endpoint not found for given token") } - return target, nil + return e, nil } -// IsPermitted checks if a specific token is permitted to access a path. -func (a *Authorization) IsPermitted(path string, token string) error { - e, err := a.GetEndpointForToken(token) +func (a *Authorizer) IsPermitted(path string, token string) error { + e, err := a.GetEndpointByToken(token) if err != nil { return err } @@ -126,21 +123,30 @@ func (a *Authorization) IsPermitted(path string, token string) error { return fmt.Errorf("token not permitted for path %s", path) } -// GetEndpointForToken returns an endpoint for the specified token. -func (a *Authorization) GetEndpointForToken(token string) (*Endpoint, error) { - e, ok := a.endpoints[token] +func (a *Authorizer) UpdateRequest(ctx context.Context, req *http.Request, token string) (*http.Request, *url.URL, error) { + e, err := a.GetEndpointByToken(token) + if err != nil { + return nil, nil, err + } + provider, ok := a.providers[e.ID()] if !ok { - return nil, errors.New("endpoint not found for token") + return nil, nil, fmt.Errorf("provider not found for id %s", e.ID()) } - return e, nil -} -func randomSecureToken() (string, error) { - b := make([]byte, tokenLenght) - _, err := rand.Read(b) + host := provider.getHost(e, req.URL.Path) + path := provider.getPath(e, req.URL.Path) + authorizationHeader, err := provider.getAuthorizationHeader(ctx, req.URL.Path) + if err != nil { + return nil, nil, err + } + url, err := url.Parse(fmt.Sprintf("%s://%s", e.scheme, host)) if err != nil { - return "", err + return nil, nil, fmt.Errorf("invalid url format: %w", err) } - randStr := base64.URLEncoding.EncodeToString(b) - return randStr, nil + + req.Host = host + req.URL.Path = path + req.Header.Del("Authorization") + req.Header.Add("Authorization", authorizationHeader) + return req, url, nil } diff --git a/pkg/auth/azure_devops.go b/pkg/auth/azure_devops.go new file mode 100644 index 0000000..4e89de5 --- /dev/null +++ b/pkg/auth/azure_devops.go @@ -0,0 +1,47 @@ +package auth + +import ( + "context" + b64 "encoding/base64" + "fmt" + "regexp" +) + +type azureDevops struct { + pat string +} + +func newAzureDevops(pat string) *azureDevops { + return &azureDevops{ + pat: pat, + } +} + +func (a *azureDevops) getPathRegex(organization, project, repository string) ([]*regexp.Regexp, error) { + baseApi, err := regexp.Compile(fmt.Sprintf(`/%s/_apis\b`, organization)) + if err != nil { + return nil, fmt.Errorf("invalid base api regex: %w", err) + } + git, err := regexp.Compile(fmt.Sprintf(`/%s/%s/_git/%s(/.*)?\b`, organization, project, repository)) + if err != nil { + return nil, err + } + api, err := regexp.Compile(fmt.Sprintf(`/%s/%s/_apis/git/repositories/%s(/.*)?\b`, organization, project, repository)) + if err != nil { + return nil, err + } + return []*regexp.Regexp{baseApi, git, api}, nil +} + +func (a *azureDevops) getAuthorizationHeader(ctx context.Context, path string) (string, error) { + tokenB64 := b64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("pat:%s", a.pat))) + return fmt.Sprintf("Basic %s", tokenB64), nil +} + +func (a *azureDevops) getHost(e *Endpoint, path string) string { + return e.host +} + +func (a *azureDevops) getPath(e *Endpoint, path string) string { + return path +} diff --git a/pkg/auth/auth_test.go b/pkg/auth/azure_devops_test.go similarity index 56% rename from pkg/auth/auth_test.go rename to pkg/auth/azure_devops_test.go index 3ca8fef..6d18cd8 100644 --- a/pkg/auth/auth_test.go +++ b/pkg/auth/azure_devops_test.go @@ -4,16 +4,17 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/xenitab/azdo-proxy/pkg/config" ) -func auth() Authorization { - config := &config.Configuration{ +func getAzureDevOpsAuthorizer() *Authorizer { + cfg := &config.Configuration{ Organizations: []*config.Organization{ { - Domain: "foo", - Pat: "", - Name: "org", + Provider: config.AzureDevOpsProviderType, + Host: "foo", + Name: "org", Repositories: []*config.Repository{ { Project: "proj", @@ -31,96 +32,96 @@ func auth() Authorization { }, }, } - auth, err := NewAuthorization(config) + auth, err := NewAuthorizer(cfg) if err != nil { panic(err) } return auth } -func TestPermitted(t *testing.T) { - authz := auth() - endpoint, err := authz.LookupEndpoint("foo", "org", "proj", "repo") +func TestAzureDevOpsPermitted(t *testing.T) { + authz := getAzureDevOpsAuthorizer() + endpoint, err := authz.GetEndpointById("foo-org-proj-repo") require.NoError(t, err) path := "/org/proj/_git/repo" err = authz.IsPermitted(path, endpoint.Token) require.NoError(t, err, "token should be permitted") } -func TestPermittedExtraPath(t *testing.T) { - authz := auth() - endpoint, err := authz.LookupEndpoint("foo", "org", "proj", "repo") +func TestAzureDevOpsPermittedExtraPath(t *testing.T) { + authz := getAzureDevOpsAuthorizer() + endpoint, err := authz.GetEndpointById("foo-org-proj-repo") require.NoError(t, err) path := "/org/proj/_git/repo/foobar/foobar" err = authz.IsPermitted(path, endpoint.Token) require.NoError(t, err, "token should be permitted") } -func TestWrongOrg(t *testing.T) { - authz := auth() - endpoint, err := authz.LookupEndpoint("foo", "org", "proj", "repo") +func TestAzureDevOpsWrongOrg(t *testing.T) { + authz := getAzureDevOpsAuthorizer() + endpoint, err := authz.GetEndpointById("foo-org-proj-repo") require.NoError(t, err) path := "/org1/proj/_git/repo" err = authz.IsPermitted(path, endpoint.Token) require.Error(t, err, "token should not be permitted") } -func TestToShortPath(t *testing.T) { - authz := auth() - endpoint, err := authz.LookupEndpoint("foo", "org", "foobar", "foobar") +func TestAzureDevOpsToShortPath(t *testing.T) { + authz := getAzureDevOpsAuthorizer() + endpoint, err := authz.GetEndpointById("foo-org-foobar-foobar") require.NoError(t, err) path := "/foobar/foobar/foobar" err = authz.IsPermitted(path, endpoint.Token) require.Error(t, err, "token should not be permitted") } -func TestWrongProject(t *testing.T) { - authz := auth() - endpoint, err := authz.LookupEndpoint("foo", "org", "proj", "repo") +func TestAzureDevOpsWrongProject(t *testing.T) { + authz := getAzureDevOpsAuthorizer() + endpoint, err := authz.GetEndpointById("foo-org-proj-repo") require.NoError(t, err) path := "/org/proj1/_git/repo" err = authz.IsPermitted(path, endpoint.Token) require.Error(t, err, "token should not be permitted") } -func TestWrongRepo(t *testing.T) { - authz := auth() - endpoint, err := authz.LookupEndpoint("foo", "org", "proj", "repo") +func TestAzureDevOpsWrongRepo(t *testing.T) { + authz := getAzureDevOpsAuthorizer() + endpoint, err := authz.GetEndpointById("foo-org-proj-repo") require.NoError(t, err) path := "/org/proj/_git/repo123" err = authz.IsPermitted(path, endpoint.Token) require.Error(t, err, "token should not be permitted") } -func TestInvalidToken(t *testing.T) { - authz := auth() +func TestAzureDevOpsInvalidToken(t *testing.T) { + authz := getAzureDevOpsAuthorizer() token := "token1" path := "/org/proj/_git/repo" err := authz.IsPermitted(path, token) require.Error(t, err, "token should not be permitted") } -func TestWhitespace(t *testing.T) { - authz := auth() - endpoint, err := authz.LookupEndpoint("foo", "org", "proj%20space", "repo%20space") +func TestAzureDevOpsWhitespace(t *testing.T) { + authz := getAzureDevOpsAuthorizer() + endpoint, err := authz.GetEndpointById("foo-org-proj%20space-repo%20space") require.NoError(t, err) path := "/org/proj%20space/_git/repo%20space" err = authz.IsPermitted(path, endpoint.Token) require.NoError(t, err, "token should be permitted") } -func TestBaseApi(t *testing.T) { - authz := auth() - endpoint, err := authz.LookupEndpoint("foo", "org", "proj", "repo") +func TestAzureDevOpsBaseApi(t *testing.T) { + authz := getAzureDevOpsAuthorizer() + endpoint, err := authz.GetEndpointById("foo-org-proj-repo") require.NoError(t, err) path := "/org/_apis" err = authz.IsPermitted(path, endpoint.Token) require.NoError(t, err, "token should be permitted") } -func TestApiPath(t *testing.T) { - authz := auth() - endpoint, err := authz.LookupEndpoint("foo", "org", "proj", "repo") +func TestAzureDevOpsApiPath(t *testing.T) { + authz := getAzureDevOpsAuthorizer() + endpoint, err := authz.GetEndpointById("foo-org-proj-repo") require.NoError(t, err) path := "/org/proj/_apis/git/repositories/repo/commits" err = authz.IsPermitted(path, endpoint.Token) diff --git a/pkg/auth/endpoint.go b/pkg/auth/endpoint.go new file mode 100644 index 0000000..98f6d8d --- /dev/null +++ b/pkg/auth/endpoint.go @@ -0,0 +1,28 @@ +package auth + +import ( + "regexp" + "strings" +) + +type Endpoint struct { + scheme string + host string + organization string + project string + repository string + regexes []*regexp.Regexp + + Token string + Namespaces []string + SecretName string +} + +func (e *Endpoint) ID() string { + comps := []string{e.host, e.organization} + if e.project != "" { + comps = append(comps, e.project) + } + comps = append(comps, e.repository) + return strings.Join(comps, "-") +} diff --git a/pkg/auth/github.go b/pkg/auth/github.go new file mode 100644 index 0000000..a2a9468 --- /dev/null +++ b/pkg/auth/github.go @@ -0,0 +1,73 @@ +package auth + +import ( + "context" + b64 "encoding/base64" + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/bradleyfalzon/ghinstallation/v2" +) + +const standardGitHub = "github.com" + +type GitHubTokenSource interface { + Token(ctx context.Context) (string, error) +} + +type github struct { + itr GitHubTokenSource +} + +func newGithub(appID, installationID int64, privateKey []byte) (*github, error) { + itr, err := ghinstallation.New(http.DefaultTransport, appID, installationID, privateKey) + if err != nil { + return nil, err + } + return &github{itr: itr}, nil +} + +func (g *github) getPathRegex(organization, project, repository string) ([]*regexp.Regexp, error) { + git, err := regexp.Compile(fmt.Sprintf(`/%s/%s(/.*)?\b`, organization, repository)) + if err != nil { + return nil, err + } + api, err := regexp.Compile(fmt.Sprintf(`/api/v3/(.*)/%s/%s/(/.*)?\b`, organization, repository)) + if err != nil { + return nil, err + } + return []*regexp.Regexp{git, api}, nil +} + +func (g *github) getAuthorizationHeader(ctx context.Context, path string) (string, error) { + token, err := g.itr.Token(ctx) + if err != nil { + return "", fmt.Errorf("error when fetching GitHub JWT token: %w", err) + } + + if strings.HasPrefix(path, "/api/v3/") { + return fmt.Sprintf("Bearer %s", token), nil + } + tokenB64 := b64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("x-access-token:%s", token))) + return fmt.Sprintf("Basic %s", tokenB64), nil +} + +func (g *github) getHost(e *Endpoint, path string) string { + if e.host != standardGitHub { + return e.host + } + if strings.HasPrefix(path, "/api/v3/") { + return fmt.Sprintf("api.%s", e.host) + } + return e.host +} + +func (g *github) getPath(e *Endpoint, path string) string { + if e.host != standardGitHub { + return path + } + newPath := strings.TrimPrefix(path, "/api/v3") + return newPath +} diff --git a/pkg/auth/github_test.go b/pkg/auth/github_test.go new file mode 100644 index 0000000..20e5ba7 --- /dev/null +++ b/pkg/auth/github_test.go @@ -0,0 +1,206 @@ +package auth + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/xenitab/azdo-proxy/pkg/config" +) + +type MockGitHubTokenSource struct { +} + +func (*MockGitHubTokenSource) Token(ctx context.Context) (string, error) { + return "foo", nil +} + +func getGitHubAuthorizer() *Authorizer { + cfg := &config.Configuration{ + Organizations: []*config.Organization{ + { + Provider: config.GitHubProviderType, + GitHub: config.GitHub{ + AppID: 123, + InstallationID: 123, + // nolint:lll // only for testing + PrivateKey: "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBMUtXT08wVzNlbDdQQkhBK0VDcFBXREIrWjVxdnJXZDN2MU9QbG81cjFMZ3U5bkRqCkFkVDRwZkR1aE92SzhZMGlaWTB0bGJBV0dpOUpFMTFFeXV1T0RoWTloYTlKaHRYOGx6UDlwL3pOdHRlNGlINkYKQWZSQjFWbk9CZ1JzOGJYUWxvL0RDRXpZdWJUZjgycTJRZ2JJWkp1UlJZOEZ3KzVYS2c2VVRKQzI1RjBmN2M0awpoNWdaS1FiMXpVR3FvQTl5a0M5Z1N0YmVRMys5Mlh2NlhpdjVnNjU4ZGN2djFpY1RPdlE4YXdORWVWd3FhbXEyClJQQ2lwWGluaS9kTHlFTVFWSm45OVFvTVZXa1krZnd2aGhQWXRtbVFrOE13QlNYMFF1dDRJdmNZdkZ5Tk83MEkKV0V4dk92R3JubTVhTjg4QUFZTkpNcHVnV1M1M3ZUTlpCeHF0L1FJREFRQUJBb0lCQUYyd3I5RUhyNFpYL1dnYwpPQXdSU0RJMzg0bWNTdWpnM0k3TXQwZ0RhaGtvS1hEbFhlOXhzVGdUeGxPRVBEOWZDcGVwc3pydmdWMTZGZjFWCks3a29QY2VSSHZ3bXRnT1ZocHZzQ1VlWmg5MldnRFNMWWZqeGNJd2E3RDRVZHhlc0hzSW5oeXZDQi84U1pWV3YKWDZ3SnB3TkUwNlhORlNJMWdld0N6bTVKbUh0V2lxbFZZcjZaYms0ekZnOTVPM21tS2hLd2lUM0ljb0VZK01rcQpWeENDNlFTRUcvc3M0dTBLSkZGVWJPdndJVDdpQ0RIUlh6YURUSVd3dlpRbWVNeEJxdFpWNFZEbEpTS2FIRHBQCnhmVjIxNFRYQkk3QlZGaFpmR0pNd2lBNXdyVjNoUkw0N1JkbURYaUxIRW1jSjRPZmRXV0RvMm5NRjlMRjVvUG4KaFNLTnRDRUNnWUVBOUQyNG45ZTgzeHFKaHlIaGhlV00yTkozRWF3Rk5oTVZDNkNnNjY0ZUZmL25YbG1TN2x4Ygo0L1o0T2w5UWwxUW9JYWNKQjRaYnIrUlpnZ3lGeDRTT0M0MDNZWkFneXJDUURiMVczUkxLSnlEQkZuWXArL0I4CjlSZ0pESVNaNTFpeFYrUk9YWWR3UUJpZzVqTHJNcVFhYndsUUJxSzBaek05SW5OUGtqdmJOeGtDZ1lFQTN1SnUKdWEyTy9aaFpoZ1Jab3V6b0ZMcVJNMTk1d0FHM1pTK1pReUZYZUY0bWRMNk1QS3hob0JBYmh6S3I2MEpPcGcvRApxSVdOYmdRRlNwb24xNkhMdEpad2lzd3RDUUx0OWJkQ2dxU0tCaXJMT2pqSU1QaStSS2FVTDA0QlkxMGtPd215CmEweGkwa0p1WUNYOTNYTzFlckxwNTY4Tm0wMThrNjNUSXBTM1BvVUNnWUFuR0FJSFE4YnRoeGZnVTJIL3hxQm0KekRsVzBNdjh2YzB1a1VWd3Mrd0k1VzhwUVBrdHdnYkxWRlltTWI5Nm1YUGEveHVJNHM2bU5zekU3akF6b1ZvRApLMVZqL21maFNhV2xMVnRNQTRmci8yZ29xajFLSUZKQUFOcmg4QSthWWkzd3ZaQjFsQW81bURlWTRTbVliMy96CnFlL3ZQL2ZVVlBWQ0lHYnFKejZOY1FLQmdRQ21DZDBlcWJMYUxJS1VtZTBFdUtQenZVQ3FHcmdpVjZUOTFrWEEKZ3JnY3pWYXNwYjdtL0N3R0I3bmFMOTl1OVFpT0lUUksrS0x4a0VFNDREcEtJeGdUd2ZhNUQzMkZOdzk2ZXprcgpCZFJrMzhCaDhTY0JoR3lKeSthY2p1bnQwZGRKdStHVW1XVU02Ync4R0ZGVWhmeHVHWmF5cCsvbEFBYU1KWFFpClVOTnAyUUtCZ0FSMGtyaFlSejJJc3gwRDF5UXM3a2dMajdtMWY2YkFxbE5IZ1ZMYTJicy9NQVc3SHpJeWp4dDMKV1dFeWxldmJPTzRCRXFnL1FCMTRwNWFNbVhFbjlHYXg1Z2RhM2YxeURTemR5M2t5My9GU3Q0dzJteVpOaFViWApmdEU4TG01OHc3eEh2MFZBc0tXRk01RFZXZVVyaDJLNFNxczdrODY1R1JKQS9xWnRqMUNrCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==", + }, + Host: "github.com", + Name: "org", + Repositories: []*config.Repository{ + { + Name: "repo", + }, + { + Name: "foobar", + }, + { + Name: "repo%20space", + }, + }, + }, + }, + } + auth, err := NewAuthorizer(cfg) + if err != nil { + panic(err) + } + return auth +} + +func TestGitHubAuthorization(t *testing.T) { + tests := []struct { + name string + path string + allow bool + }{ + { + name: "allow repo", + path: "/org/repo", + allow: true, + }, + { + name: "allow api", + path: "/api/v3/org/repo", + allow: true, + }, + { + name: "disallow wrong repo", + path: "/org/foo", + allow: false, + }, + { + name: "disallow wront repo in api", + path: "/api/v3/org/foo", + allow: false, + }, + { + name: "disallow wrong org", + path: "/foo/repo", + allow: false, + }, + { + name: "disallow wront org in api", + path: "/api/v3/foo/repo", + allow: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + authz := getGitHubAuthorizer() + endpoint, err := authz.GetEndpointById("github.com-org-repo") + require.NoError(t, err) + err = authz.IsPermitted(tt.path, endpoint.Token) + + if tt.allow { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} + +func TestGithubApiGetAuthorization(t *testing.T) { + gh := &github{itr: &MockGitHubTokenSource{}} + authorization, err := gh.getAuthorizationHeader(context.TODO(), "/api/v3/test") + require.NoError(t, err) + require.Equal(t, "Bearer foo", authorization) +} + +func TestGithubGitGetAuthorization(t *testing.T) { + gh := &github{itr: &MockGitHubTokenSource{}} + authorization, err := gh.getAuthorizationHeader(context.TODO(), "/org/repo") + require.NoError(t, err) + require.Equal(t, "Basic eC1hY2Nlc3MtdG9rZW46Zm9v", authorization) +} + +func TestGetHost(t *testing.T) { + tests := []struct { + name string + host string + path string + expected string + }{ + { + name: "api path standard github", + host: standardGitHub, + path: "/api/v3/test", + expected: fmt.Sprintf("api.%s", standardGitHub), + }, + { + name: "repo path standard github", + host: standardGitHub, + path: "/foo/bar", + expected: standardGitHub, + }, + { + name: "api path enterprise github", + host: "example.com", + path: "/api/v3/test", + expected: "example.com", + }, + { + name: "repo path enterprise github", + host: "example.com", + path: "/foo/bar", + expected: "example.com", + }, + } + gh := &github{itr: &MockGitHubTokenSource{}} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Endpoint{ + host: tt.host, + } + host := gh.getHost(e, tt.path) + require.Equal(t, tt.expected, host) + }) + } +} + +func TestGetPath(t *testing.T) { + tests := []struct { + name string + host string + path string + expected string + }{ + { + name: "api path standard github", + host: standardGitHub, + path: "/api/v3/test", + expected: "/test", + }, + { + name: "repo path standard github", + host: standardGitHub, + path: "/foo/bar", + expected: "/foo/bar", + }, + { + name: "api path enterprise github", + host: "example.com", + path: "/api/v3/test", + expected: "/api/v3/test", + }, + { + name: "repo path enterprise github", + host: "example.com", + path: "/foo/bar", + expected: "/foo/bar", + }, + } + gh := &github{itr: &MockGitHubTokenSource{}} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Endpoint{ + host: tt.host, + } + path := gh.getPath(e, tt.path) + require.Equal(t, tt.expected, path) + }) + } +} diff --git a/pkg/auth/util.go b/pkg/auth/util.go new file mode 100644 index 0000000..9c25ad9 --- /dev/null +++ b/pkg/auth/util.go @@ -0,0 +1,18 @@ +package auth + +import ( + "crypto/rand" + "encoding/base64" +) + +const tokenLenght = 64 + +func randomSecureToken() (string, error) { + b := make([]byte, tokenLenght) + _, err := rand.Read(b) + if err != nil { + return "", err + } + randStr := base64.URLEncoding.EncodeToString(b) + return randStr, nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go index a920a94..af7843a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,8 +1,14 @@ package config import ( - "fmt" - "net/url" + "strings" +) + +type ProviderType string + +const ( + AzureDevOpsProviderType = "azuredevops" + GitHubProviderType = "github" ) type Configuration struct { @@ -10,26 +16,37 @@ type Configuration struct { } type Organization struct { - Name string `json:"name" validate:"required"` - Domain string `json:"domain,omitempty" validate:"required"` + Provider ProviderType `json:"provider" validate:"required"` + AzureDevOps AzureDevOps `json:"azuredevops"` + GitHub GitHub `json:"github"` + Host string `json:"host,omitempty" validate:"required"` Scheme string `json:"scheme,omitempty" validate:"required"` - Pat string `json:"pat" validate:"required"` + Name string `json:"name" validate:"required"` Repositories []*Repository `json:"repositories" validate:"required,dive"` } -func (o *Organization) GetTarget() (*url.URL, error) { - u, err := url.Parse(fmt.Sprintf("%s://%s", o.Scheme, o.Domain)) - if err != nil { - return nil, err - } - return u, nil +type AzureDevOps struct { + Pat string `json:"pat"` +} + +type GitHub struct { + AppID int64 `json:"appID"` + InstallationID int64 `json:"installationID"` + PrivateKey string `json:"privateKey"` } func (o *Organization) GetSecretName(r *Repository) string { if r.SecretNameOverride != "" { return r.SecretNameOverride } - return fmt.Sprintf("%s-%s-%s", o.Name, r.Project, r.Name) + + comps := []string{} + comps = append(comps, o.Name) + if r.Project != "" { + comps = append(comps, r.Project) + } + comps = append(comps, r.Name) + return strings.Join(comps, "-") } type Repository struct { diff --git a/pkg/config/load.go b/pkg/config/load.go index 02e1d14..cf6fe40 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -2,19 +2,18 @@ package config import ( "encoding/json" - iofs "io/fs" "github.com/go-playground/validator/v10" + "github.com/spf13/afero" ) const ( - defaultDomain = "dev.azure.com" defaultScheme = "https" ) // LoadConfiguration parses and validates the configuration file at a given path. -func LoadConfiguration(fs iofs.FS, path string) (*Configuration, error) { - b, err := iofs.ReadFile(fs, path) +func LoadConfiguration(fs afero.Fs, path string) (*Configuration, error) { + b, err := afero.ReadFile(fs, path) if err != nil { return nil, err } @@ -34,9 +33,6 @@ func LoadConfiguration(fs iofs.FS, path string) (*Configuration, error) { func setConfigurationDefaults(cfg *Configuration) *Configuration { for i, o := range cfg.Organizations { - if o.Domain == "" { - cfg.Organizations[i].Domain = defaultDomain - } if o.Scheme == "" { cfg.Organizations[i].Scheme = defaultScheme } diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go index 4151aa5..4c59591 100644 --- a/pkg/config/load_test.go +++ b/pkg/config/load_test.go @@ -1,7 +1,6 @@ package config import ( - iofs "io/fs" "testing" "github.com/spf13/afero" @@ -14,9 +13,12 @@ const validJson = ` { "organizations": [ { - "domain": "dev.azure.com", + "provider": "azuredevops", + "host": "dev.azure.com", "name": "xenitab", - "pat": "foobar", + "azuredevops": { + "pat": "foobar" + }, "repositories": [ { "name": "gitops-deployment", @@ -31,7 +33,7 @@ const validJson = ` const invalidJson = ` { - "domain": "dev.azure.com", + "host": "dev.azure.com", }} } ` @@ -40,7 +42,8 @@ const missingPatJson = ` { "organizations": [ { - "domain": "dev.azure.com", + "provider": "azuredevops", + "host": "dev.azure.com", "name": "xenitab", "repositories": [ { @@ -57,15 +60,15 @@ const missingRepositoriesJson = ` { "organizations": [ { - "domain": "dev.azure.com", + "provider": "azuredevops", + "host": "dev.azure.com", "name": "xenitab", - "pat": "foobar" } ] } ` -func fsWithContent(path string, content string) (iofs.FS, error) { +func fsWithContent(path string, content string) (afero.Fs, error) { fs := afero.NewMemMapFs() file, err := fs.Create(path) if err != nil { @@ -76,7 +79,7 @@ func fsWithContent(path string, content string) (iofs.FS, error) { if err != nil { return nil, err } - return afero.NewIOFS(fs), nil + return fs, nil } func TestValidJson(t *testing.T) { @@ -85,10 +88,10 @@ func TestValidJson(t *testing.T) { cfg, err := LoadConfiguration(fs, path) require.NoError(t, err) require.NotEmpty(t, cfg.Organizations) - require.Equal(t, "dev.azure.com", cfg.Organizations[0].Domain) + require.Equal(t, "dev.azure.com", cfg.Organizations[0].Host) require.Equal(t, "https", cfg.Organizations[0].Scheme) require.Equal(t, "xenitab", cfg.Organizations[0].Name) - require.Equal(t, "foobar", cfg.Organizations[0].Pat) + require.Equal(t, "foobar", cfg.Organizations[0].AzureDevOps.Pat) require.NotEmpty(t, cfg.Organizations[0].Repositories) require.Equal(t, "gitops-deployment", cfg.Organizations[0].Repositories[0].Name) require.Equal(t, "Lab", cfg.Organizations[0].Repositories[0].Project) diff --git a/pkg/server/server.go b/pkg/server/server.go index d03b05b..f0ab6b2 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,9 +2,6 @@ package server import ( "context" - "encoding/base64" - "errors" - "fmt" "net/http" "net/http/httputil" @@ -17,21 +14,12 @@ import ( "github.com/xenitab/azdo-proxy/pkg/auth" ) -type AzdoServer struct { +type Server struct { logger logr.Logger srv *http.Server } -func NewAzdoServer(logger logr.Logger, port string, authz auth.Authorization) (*AzdoServer, error) { - proxies := map[string]*httputil.ReverseProxy{} - for k := range authz.GetEndpoints() { - target, err := authz.GetTargetForToken(k) - if err != nil { - return nil, fmt.Errorf("could not create http proxy from endpoints: %w", err) - } - proxies[target.String()] = httputil.NewSingleHostReverseProxy(target) - } - +func NewServer(logger logr.Logger, port string, authz *auth.Authorizer) *Server { router := mux.NewRouter() prometheus_mdlw := middleware.New(middleware.Config{ Recorder: prommetrics.NewRecorder(prommetrics.Config{ @@ -41,72 +29,55 @@ func NewAzdoServer(logger logr.Logger, port string, authz auth.Authorization) (* router.Use(std.HandlerProvider("", prometheus_mdlw)) router.HandleFunc("/readyz", readinessHandler(logger)).Methods("GET") router.HandleFunc("/healthz", livenessHandler(logger)).Methods("GET") - router.PathPrefix("/").HandlerFunc(proxyHandler(logger, proxies, authz)) + router.PathPrefix("/").HandlerFunc(proxyHandler(logger, authz)) srv := &http.Server{Addr: port, Handler: router} - return &AzdoServer{ + return &Server{ logger: logger.WithName("azdo-server"), srv: srv, - }, nil + } } -// ListenAndServe starts the HTTP server on the specified port. -func (a *AzdoServer) ListenAndServe() error { - return a.srv.ListenAndServe() +func (s *Server) ListenAndServe() error { + return s.srv.ListenAndServe() } -func (a *AzdoServer) Shutdown(ctx context.Context) error { - return a.srv.Shutdown(ctx) +func (s *Server) Shutdown(ctx context.Context) error { + return s.srv.Shutdown(ctx) } -//nolint:lll // difficult to make this shorter right now (Philip) -func proxyHandler(logger logr.Logger, proxies map[string]*httputil.ReverseProxy, authz auth.Authorization) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { +func proxyHandler(logger logr.Logger, authz *auth.Authorizer) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + ctx := context.Background() + handlerLogger := logger.WithValues("path", req.URL.Path) + // Get the token from the request - token, err := tokenFromRequest(r) + token, err := getTokenFromRequest(req) if err != nil { w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"") http.Error(w, "Missing basic authentication", http.StatusUnauthorized) return } - - handlerLogger := logger.WithValues("path", r.URL.Path) - // Check basic auth with local auth configuration - err = authz.IsPermitted(r.URL.EscapedPath(), token) + err = authz.IsPermitted(req.URL.EscapedPath(), token) if err != nil { handlerLogger.Error(err, "Received unauthorized request") http.Error(w, "User not permitted", http.StatusForbidden) return } - pat, err := authz.GetPatForToken(token) - if err != nil { - handlerLogger.Error(err, "Could not find the PAT for the given token") - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - target, err := authz.GetTargetForToken(token) + // Authenticate the request with the proper token + req, url, err := authz.UpdateRequest(ctx, req, token) if err != nil { - handlerLogger.Error(err, "Target could not be created from the token") + handlerLogger.Error(err, "Could not authenticate request") http.Error(w, "Internal server error", http.StatusInternalServerError) return } - - // Overwrite the authorization header with the PAT token handlerLogger.Info("Authenticated request") - r.Host = target.Host - r.Header.Del("Authorization") - patB64 := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("pat:%s", pat))) - r.Header.Add("Authorization", "Basic "+patB64) + // TODO (Philip): Add caching of the proxy // Forward the request to the correct proxy - proxy, ok := proxies[target.String()] - if !ok { - handlerLogger.Info("missing proxy for target") - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - proxy.ServeHTTP(w, r) + proxy := httputil.NewSingleHostReverseProxy(url) + proxy.ServeHTTP(w, req) } } @@ -129,21 +100,3 @@ func livenessHandler(log logr.Logger) func(http.ResponseWriter, *http.Request) { } } } - -func tokenFromRequest(r *http.Request) (string, error) { - username, password, ok := r.BasicAuth() - if !ok { - return "", fmt.Errorf("basic auth not present") - } - - token := username - if len(password) > 0 { - token = password - } - - if token == "" { - return "", errors.New("token cannot be empty") - } - - return token, nil -} diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go deleted file mode 100644 index 1c57f39..0000000 --- a/pkg/server/server_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package server - -import ( - "encoding/base64" - "fmt" - "net/http" - "testing" - - "github.com/stretchr/testify/require" -) - -const testToken = "foobar" - -func basicAuthHeader(username, password string) http.Header { - basicAuth := fmt.Sprintf("%s:%s", username, password) - b64TestToken := base64.StdEncoding.EncodeToString([]byte(basicAuth)) - tokenData := fmt.Sprintf("Basic %s", b64TestToken) - return http.Header{ - "Authorization": {tokenData}, - } -} - -func TestTokenFromRequestUsername(t *testing.T) { - r := http.Request{ - Header: basicAuthHeader(testToken, ""), - } - token, err := tokenFromRequest(&r) - require.NoError(t, err) - require.Equal(t, testToken, token) -} - -func TestTokenFromRequestPassword(t *testing.T) { - r := http.Request{ - Header: basicAuthHeader("", testToken), - } - token, err := tokenFromRequest(&r) - require.NoError(t, err) - require.Equal(t, testToken, token) -} - -func TestTokenFromRequestBoth(t *testing.T) { - r := http.Request{ - Header: basicAuthHeader(testToken, testToken), - } - token, err := tokenFromRequest(&r) - require.NoError(t, err) - require.Equal(t, testToken, token) -} - -func TestTokenFromRequestNone(t *testing.T) { - r := http.Request{ - Header: basicAuthHeader("", ""), - } - _, err := tokenFromRequest(&r) - require.Error(t, err) - require.EqualError(t, err, "token cannot be empty") -} - -func TestTokenFromRequestWrongFormat(t *testing.T) { - r := http.Request{ - Header: http.Header{ - "Authorization": {"foobar"}, - }, - } - _, err := tokenFromRequest(&r) - require.Error(t, err) - require.EqualError(t, err, "basic auth not present") -} diff --git a/pkg/server/token.go b/pkg/server/token.go new file mode 100644 index 0000000..5bd313e --- /dev/null +++ b/pkg/server/token.go @@ -0,0 +1,53 @@ +package server + +import ( + b64 "encoding/base64" + "fmt" + "net/http" + "strings" +) + +const ( + headerKey = "Authorization" + basicKey = "Basic " + bearerKey = "Bearer " +) + +func getTokenFromRequest(req *http.Request) (string, error) { + headerValue := req.Header.Get(headerKey) + if headerValue == "" { + return "", fmt.Errorf("Header %s not found in request", headerKey) + } + encodedToken, isBearer, err := extractEncodedToken(headerValue) + if err != nil { + return "", err + } + if isBearer { + return encodedToken, nil + } + decodedToken, err := b64.URLEncoding.DecodeString(encodedToken) + if err != nil { + return "", err + } + comps := strings.Split(string(decodedToken), ":") + if len(comps) != 2 { + return "", fmt.Errorf("decoded token does not contain enough components") + } + if comps[1] != "" { + return comps[1], nil + } + if comps[0] != "" { + return comps[0], nil + } + return "", fmt.Errorf("username component and password component cannot be empty") +} + +func extractEncodedToken(value string) (string, bool, error) { + if strings.HasPrefix(value, basicKey) { + return strings.TrimPrefix(value, basicKey), false, nil + } + if strings.HasPrefix(value, bearerKey) { + return strings.TrimPrefix(value, bearerKey), true, nil + } + return "", false, fmt.Errorf("Missing either %s or %s", basicKey, bearerKey) +} diff --git a/pkg/server/token_test.go b/pkg/server/token_test.go new file mode 100644 index 0000000..1234c36 --- /dev/null +++ b/pkg/server/token_test.go @@ -0,0 +1,62 @@ +package server + +import ( + b64 "encoding/base64" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBasic(t *testing.T) { + tests := []struct { + name string + key string + username string + password string + expected string + }{ + { + name: "basic only username", + key: basicKey, + username: "foo", + password: "", + expected: "foo", + }, + { + name: "basic only password", + key: basicKey, + username: "", + password: "bar", + expected: "bar", + }, + { + name: "basic username and password", + key: basicKey, + username: "foo", + password: "bar", + expected: "bar", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := &http.Request{Header: http.Header{}} + combo := fmt.Sprintf("%s:%s", tt.username, tt.password) + headerValue := fmt.Sprintf("%s%s", tt.key, b64.URLEncoding.EncodeToString([]byte(combo))) + req.Header.Set(headerKey, headerValue) + token, err := getTokenFromRequest(req) + require.NoError(t, err) + require.Equal(t, tt.expected, token) + }) + } +} + +func TestBearer(t *testing.T) { + req := &http.Request{Header: http.Header{}} + headerValue := fmt.Sprintf("%s%s", bearerKey, "token") + req.Header.Set(headerKey, headerValue) + token, err := getTokenFromRequest(req) + require.NoError(t, err) + require.Equal(t, "token", token) +} diff --git a/pkg/token/token_writer.go b/pkg/token/token_writer.go index 2f46e51..c8382cc 100644 --- a/pkg/token/token_writer.go +++ b/pkg/token/token_writer.go @@ -20,7 +20,7 @@ import ( type TokenWriter struct { logger logr.Logger client kubernetes.Interface - authz auth.Authorization + authz *auth.Authorizer } const ( @@ -30,7 +30,7 @@ const ( tokenKey = "token" ) -func NewTokenWriter(logger logr.Logger, client kubernetes.Interface, authz auth.Authorization) *TokenWriter { +func NewTokenWriter(logger logr.Logger, client kubernetes.Interface, authz *auth.Authorizer) *TokenWriter { return &TokenWriter{ logger: logger, client: client, @@ -63,7 +63,7 @@ func (t *TokenWriter) Start(ctx context.Context) error { // initial write of the new secrets for _, e := range t.authz.GetEndpoints() { - labels := createSecretLabels(e.Domain, e.Organization, e.Project, e.Repository) + labels := createSecretLabels(e.ID()) for _, ns := range e.Namespaces { err := t.createSecret(ctx, e.SecretName, ns, e.Token, labels) if err != nil { @@ -118,17 +118,17 @@ func (t *TokenWriter) secretDelete(obj interface{}) { return } - domain, org, proj, repo, err := getSecretLabels(secret) - if err != nil { - t.logger.Error(err, "metadata missing in secret labels", "name", secret.Name, "namespace", secret.Namespace) + id, ok := secret.ObjectMeta.Labels["azdo-proxy.xenit.io/id"] + if !ok { + t.logger.Error(fmt.Errorf("id label not found"), "metadata missing in secret labels", "name", secret.Name, "namespace", secret.Namespace) return } - e, err := t.authz.LookupEndpoint(domain, org, proj, repo) + e, err := t.authz.GetEndpointById(id) if err != nil { t.logger.Error(err, "deleted secret does not match an endpoint", "name", secret.Name, "namespace", secret.Namespace) return } - labels := createSecretLabels(e.Domain, e.Organization, e.Project, e.Repository) + labels := createSecretLabels(e.ID()) err = t.createSecret(context.Background(), e.SecretName, secret.Namespace, e.Token, labels) if err != nil { t.logger.Error(err, "Unable to created secret after deletion") @@ -178,32 +178,9 @@ func (t *TokenWriter) deleteSecret(ctx context.Context, name string, namespace s return nil } -func createSecretLabels(domain, org, proj, repo string) map[string]string { +func createSecretLabels(id string) map[string]string { return map[string]string{ - "app.kubernetes.io/managed-by": "azdo-proxy", - "azdo-proxy.xenit.io/domain": domain, - "azdo-proxy.xenit.io/organization": org, - "azdo-proxy.xenit.io/project": proj, - "azdo-proxy.xenit.io/repository": repo, + "app.kubernetes.io/managed-by": "azdo-proxy", + "azdo-proxy.xenit.io/id": id, } } - -func getSecretLabels(secret *v1.Secret) (string, string, string, string, error) { - labels := secret.ObjectMeta.Labels - keys := []string{ - "azdo-proxy.xenit.io/domain", - "azdo-proxy.xenit.io/organization", - "azdo-proxy.xenit.io/project", - "azdo-proxy.xenit.io/repository", - } - values := []string{} - for _, k := range keys { - v, ok := labels[k] - if !ok { - return "", "", "", "", fmt.Errorf("key not found in secret labels: %v", k) - } - values = append(values, v) - } - - return values[0], values[1], values[2], values[3], nil -} diff --git a/pkg/token/token_writer_test.go b/pkg/token/token_writer_test.go index c967356..c11603f 100644 --- a/pkg/token/token_writer_test.go +++ b/pkg/token/token_writer_test.go @@ -18,9 +18,12 @@ func TestBasic(t *testing.T) { cfg := &config.Configuration{ Organizations: []*config.Organization{ { - Name: "org", - Domain: "foo", - Pat: "test", + Provider: "azuredevops", + Name: "org", + Host: "foo", + AzureDevOps: config.AzureDevOps{ + Pat: "foo", + }, Repositories: []*config.Repository{ { Project: "proj", @@ -32,7 +35,7 @@ func TestBasic(t *testing.T) { }, }, } - authz, err := auth.NewAuthorization(cfg) + authz, err := auth.NewAuthorizer(cfg) require.NoError(t, err) logger := logr.Discard() client := fake.NewSimpleClientset() @@ -46,7 +49,7 @@ func TestBasic(t *testing.T) { } }() - endpoint, err := authz.LookupEndpoint("foo", "org", "proj", "repo") + endpoint, err := authz.GetEndpointById("foo-org-proj-repo") require.NoError(t, err) require.Eventuallyf(t, func() bool { secret, err := client.CoreV1().Secrets("foo").Get(ctx, "azdo", v1.GetOptions{}) @@ -81,7 +84,7 @@ func TestBasic(t *testing.T) { return false } return true - }, 5*time.Second, 1*time.Second, "secret azdo does not have correct value in namespace bar") + }, 15*time.Second, 1*time.Second, "secret azdo does not have correct value in namespace bar") err = client.CoreV1().Secrets("bar").Delete(ctx, "azdo", v1.DeleteOptions{}) require.NoError(t, err)