├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── gangway │ ├── handlers.go │ ├── handlers_test.go │ └── main.go ├── docs ├── README.md ├── auth0.md ├── configuration.md ├── custom-templates.md ├── dex.md ├── google.md ├── images │ ├── gangway-sequence-diagram.png │ ├── goauth-add-credentials-menu.png │ ├── goauth-client-settings.png │ ├── goauth-empty.png │ └── screenshot.png └── yaml │ ├── 01-namespace.yaml │ ├── 02-config.yaml │ ├── 03-deployment.yaml │ ├── 04-service.yaml │ ├── 05-ingress.yaml │ └── role │ └── rolebinding.yaml ├── go.mod ├── go.sum ├── internal ├── config │ ├── config.go │ ├── config_test.go │ └── transport.go ├── oidc │ ├── token.go │ └── token_test.go └── session │ ├── session.go │ ├── session_test.go │ ├── store.go │ └── store_test.go └── templates ├── commandline.tmpl └── home.tmpl /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | config.yml 4 | /gangway 5 | vendor/ 6 | cmd/gangway/bindata.go 7 | .idea/ 8 | gangway.kubeconfig -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go_import_path: github.com/heptiolabs/gangway 3 | go: 4 | - 1.14.x 5 | 6 | sudo: false 7 | 8 | install: 9 | - make setup && make deps && make bindata 10 | 11 | script: 12 | - make check 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Gangway Community Code of Conduct 2 | 3 | ## Contributor Code of Conduct 4 | 5 | As contributors and maintainers of this project, and in the interest of fostering 6 | an open and welcoming community, we pledge to respect all people who contribute 7 | through reporting issues, posting feature requests, updating documentation, 8 | submitting pull requests or patches, and other activities. 9 | 10 | We are committed to making participation in this project a harassment-free experience for 11 | everyone, regardless of level of experience, gender, gender identity and expression, 12 | sexual orientation, disability, personal appearance, body size, race, ethnicity, age, 13 | religion, or nationality. 14 | 15 | Examples of unacceptable behavior by participants include: 16 | 17 | * The use of sexualized language or imagery 18 | * Personal attacks 19 | * Trolling or insulting/derogatory comments 20 | * Public or private harassment 21 | * Publishing other's private information, such as physical or electronic addresses, 22 | without explicit permission 23 | * Other unethical or unprofessional conduct. 24 | 25 | Project maintainers have the right and responsibility to remove, edit, or reject 26 | comments, commits, code, wiki edits, issues, and other contributions that are not 27 | aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers 28 | commit themselves to fairly and consistently applying these principles to every aspect 29 | of managing this project. Project maintainers who do not follow or enforce the Code of 30 | Conduct may be permanently removed from the project team. 31 | 32 | This code of conduct applies both within project spaces and in public spaces 33 | when an individual is representing the project or its community. 34 | 35 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer(s). 36 | 37 | This Code of Conduct is adapted from the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md) and [Contributor Covenant](http://contributor-covenant.org/version/1/2/0/), version 1.2.0. 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## DCO Sign off 2 | 3 | All authors to the project retain copyright to their work. However, to ensure 4 | that they are only submitting work that they have rights to, we are requiring 5 | everyone to acknowledge this by signing their work. 6 | 7 | Any copyright notices in this repos should specify the authors as "The 8 | heptio/gangway authors". 9 | 10 | To sign your work, just add a line like this at the end of your commit message: 11 | 12 | ``` 13 | Signed-off-by: Craig Tracey 14 | ``` 15 | 16 | This can easily be done with the `--signoff` option to `git commit`. 17 | 18 | By doing this you state that you can certify the following (from https://developercertificate.org/): 19 | 20 | ``` 21 | Developer Certificate of Origin 22 | Version 1.1 23 | 24 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 25 | 1 Letterman Drive 26 | Suite D4700 27 | San Francisco, CA, 94129 28 | 29 | Everyone is permitted to copy and distribute verbatim copies of this 30 | license document, but changing it is not allowed. 31 | 32 | 33 | Developer's Certificate of Origin 1.1 34 | 35 | By making a contribution to this project, I certify that: 36 | 37 | (a) The contribution was created in whole or in part by me and I 38 | have the right to submit it under the open source license 39 | indicated in the file; or 40 | 41 | (b) The contribution is based upon previous work that, to the best 42 | of my knowledge, is covered under an appropriate open source 43 | license and I have the right under that license to submit that 44 | work with modifications, whether created in whole or in part 45 | by me, under the same open source license (unless I am 46 | permitted to submit under a different license), as indicated 47 | in the file; or 48 | 49 | (c) The contribution was provided directly to me by some other 50 | person who certified (a), (b) or (c) and I have not modified 51 | it. 52 | 53 | (d) I understand and agree that this project and the contribution 54 | are public and that a record of the contribution (including all 55 | personal information I submit with it, including my sign-off) is 56 | maintained indefinitely and may be redistributed consistent with 57 | this project or the open source license(s) involved. 58 | ``` 59 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.2-stretch 2 | WORKDIR /go/src/github.com/heptiolabs/gangway 3 | 4 | RUN go get -u github.com/mjibson/esc/... 5 | COPY . . 6 | RUN esc -o cmd/gangway/bindata.go templates/ 7 | 8 | ENV GO111MODULE on 9 | RUN go mod verify 10 | RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-w -s" -v github.com/heptiolabs/gangway/... 11 | 12 | FROM debian:9.12-slim 13 | RUN apt-get update && apt-get install -y ca-certificates 14 | USER 1001:1001 15 | COPY --from=0 /go/bin/gangway /bin/gangway 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright © 2017 Heptio 2 | # Copyright © 2017 Craig Tracey 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | PROJECT := gangway 16 | # Where to push the docker image. 17 | REGISTRY ?= gcr.io/heptio-images 18 | IMAGE := $(REGISTRY)/$(PROJECT) 19 | SRCDIRS := ./cmd/gangway 20 | PKGS := $(shell go list ./cmd/... ./internal/...) 21 | 22 | VERSION ?= master 23 | 24 | all: build 25 | 26 | build: deps bindata 27 | go build ./... 28 | 29 | install: 30 | go install -v ./cmd/gangway/... 31 | 32 | setup: 33 | go get -u github.com/mjibson/esc/... 34 | 35 | check: test vet gofmt staticcheck misspell 36 | 37 | deps: 38 | GO111MODULE=on go mod tidy && GO111MODULE=on go mod vendor && GO111MODULE=on go mod verify 39 | 40 | vet: | test 41 | go vet ./... 42 | 43 | bindata: 44 | esc -o cmd/gangway/bindata.go templates/ 45 | 46 | test: 47 | go test -v ./... 48 | 49 | staticcheck: 50 | @go get honnef.co/go/tools/cmd/staticcheck 51 | staticcheck $(PKGS) 52 | 53 | misspell: 54 | @go get github.com/client9/misspell/cmd/misspell 55 | misspell \ 56 | -i clas \ 57 | -locale US \ 58 | -error \ 59 | cmd/* docs/* *.md 60 | 61 | gofmt: 62 | @echo Checking code is gofmted 63 | @test -z "$(shell gofmt -s -l -d -e $(SRCDIRS) | tee /dev/stderr)" 64 | 65 | image: 66 | docker build . -t $(IMAGE):$(VERSION) 67 | 68 | push: 69 | docker push $(IMAGE):$(VERSION) 70 | 71 | .PHONY: all deps bindata test image setup 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | gangway 3 | [![Build Status](https://travis-ci.org/heptiolabs/gangway.svg?branch=master)](https://travis-ci.org/heptiolabs/gangway) 4 | ======= 5 | 6 | # VMware has ended active development of this project, this repository will no longer be updated. 7 | 8 | _(noun): An opening in the bulwark of the ship to allow passengers to board or leave the ship._ 9 | 10 | An application that can be used to easily enable authentication flows via OIDC for a kubernetes cluster. 11 | Kubernetes supports [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) as a way to identify users who access the cluster. 12 | Gangway allows users to self-configure their `kubectl` configuration in a few short steps. 13 | 14 | ![gangway screenshot](docs/images/screenshot.png) 15 | 16 | ## Deployment 17 | 18 | Instructions for deploying gangway for common cloud providers can be found [here](docs/README.md). 19 | 20 | ## How It Works 21 | 22 | Kubernetes supports OpenID Connect (OIDC) as a user authentication mechanism. OIDC is an 23 | authentication protocol that allows servers to verify the identity of a user by way of an ID Token. 24 | 25 | When using OIDC to authenticate with Kubernetes, the client (e.g. `kubectl`) sends the ID token 26 | alongside all requests to the API server. On the server side, the Kubernetes API server verifies the 27 | token to ensure it is valid and has not expired. Once verified, the API server extracts username and 28 | group membership information from the token, and continues processing the request. 29 | 30 | In order to obtain the ID token, the user must go through the OIDC authentication process. This is 31 | where Gangway comes in. Gangway is a web application that enables the OIDC authentication flow which 32 | results in the minting of the ID Token. 33 | 34 | Gangway is configured as a client of an upstream Identity Service that speaks OIDC. To obtain the ID 35 | token, the user accesses Gangway, initiates the OIDC flow by clicking the "Log In" button, and 36 | completes the flow by authenticating with the upstream Identity Service. The user's credentials are 37 | never shared with Gangway. 38 | 39 | Once the authentication flow is complete, the user is redirected to a Gangway page that provides 40 | instructions on how to configure `kubectl` to use the ID token. 41 | 42 | The following sequence diagram details the authentication flow: 43 | 44 |

45 | 46 |

47 | 48 | ## API-Server flags 49 | 50 | gangway requires that the Kubernetes API server is configured for OIDC: 51 | 52 | https://kubernetes.io/docs/admin/authentication/#configuring-the-api-server 53 | 54 | ```bash 55 | kube-apiserver 56 | ... 57 | --oidc-issuer-url="https://example.auth0.com/" 58 | --oidc-client-id=3YM4ue8MoXgBkvCIHh00000000000 59 | --oidc-username-claim=email 60 | --oidc-groups-claim=groups 61 | ``` 62 | 63 | ## Build 64 | 65 | Requirements for building 66 | 67 | - Go (built with version >= 1.12) 68 | - [esc](https://github.com/mjibson/esc) for static resources. 69 | 70 | A Makefile is provided for building tasks. The options are as follows 71 | 72 | Getting started is as simple as: 73 | 74 | ```bash 75 | go get -u github.com/heptiolabs/gangway 76 | cd $GOPATH/src/github.com/heptiolabs/gangway 77 | make setup 78 | make 79 | ``` 80 | -------------------------------------------------------------------------------- /cmd/gangway/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Heptio 2 | // Copyright © 2017 Craig Tracey 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "crypto/rand" 20 | "encoding/base64" 21 | "fmt" 22 | htmltemplate "html/template" 23 | "io/ioutil" 24 | "net/http" 25 | "net/url" 26 | "os" 27 | "path/filepath" 28 | "strings" 29 | 30 | "github.com/dgrijalva/jwt-go" 31 | "github.com/ghodss/yaml" 32 | "github.com/heptiolabs/gangway/internal/oidc" 33 | log "github.com/sirupsen/logrus" 34 | "golang.org/x/oauth2" 35 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api/v1" 36 | ) 37 | 38 | const ( 39 | templatesBase = "/templates" 40 | ) 41 | 42 | // userInfo stores information about an authenticated user 43 | type userInfo struct { 44 | ClusterName string 45 | Username string 46 | Claims jwt.MapClaims 47 | KubeCfgUser string 48 | IDToken string 49 | RefreshToken string 50 | ClientID string 51 | ClientSecret string 52 | IssuerURL string 53 | APIServerURL string 54 | ClusterCA string 55 | HTTPPath string 56 | } 57 | 58 | // homeInfo is used to store dynamic properties on 59 | type homeInfo struct { 60 | HTTPPath string 61 | } 62 | 63 | func serveTemplate(tmplFile string, data interface{}, w http.ResponseWriter) { 64 | var ( 65 | templatePath string 66 | templateData []byte 67 | err error 68 | ) 69 | 70 | // Use custom templates if provided 71 | if cfg.CustomHTMLTemplatesDir != "" { 72 | templatePath = filepath.Join(cfg.CustomHTMLTemplatesDir, tmplFile) 73 | templateData, err = ioutil.ReadFile(templatePath) 74 | } else { 75 | templatePath = filepath.Join(templatesBase, tmplFile) 76 | // FSByte is generated by the esc file embedder 77 | // See https://github.com/mjibson/esc for more info. 78 | templateData, err = FSByte(false, templatePath) 79 | } 80 | 81 | if err != nil { 82 | log.Errorf("Failed to find template asset: %s at path: %s", tmplFile, templatePath) 83 | http.Error(w, err.Error(), http.StatusInternalServerError) 84 | return 85 | } 86 | 87 | tmpl := htmltemplate.New(tmplFile) 88 | tmpl, err = tmpl.Parse(string(templateData)) 89 | if err != nil { 90 | log.Errorf("Failed to parse template: %v", err) 91 | http.Error(w, err.Error(), http.StatusInternalServerError) 92 | } 93 | tmpl.ExecuteTemplate(w, tmplFile, data) 94 | } 95 | 96 | func generateKubeConfig(cfg *userInfo) clientcmdapi.Config { 97 | // fill out kubeconfig structure 98 | kcfg := clientcmdapi.Config{ 99 | Kind: "Config", 100 | APIVersion: "v1", 101 | CurrentContext: cfg.ClusterName, 102 | Clusters: []clientcmdapi.NamedCluster{ 103 | { 104 | Name: cfg.ClusterName, 105 | Cluster: clientcmdapi.Cluster{ 106 | Server: cfg.APIServerURL, 107 | CertificateAuthorityData: []byte(cfg.ClusterCA), 108 | }, 109 | }, 110 | }, 111 | Contexts: []clientcmdapi.NamedContext{ 112 | { 113 | Name: cfg.ClusterName, 114 | Context: clientcmdapi.Context{ 115 | Cluster: cfg.ClusterName, 116 | AuthInfo: cfg.KubeCfgUser, 117 | }, 118 | }, 119 | }, 120 | AuthInfos: []clientcmdapi.NamedAuthInfo{ 121 | { 122 | Name: cfg.KubeCfgUser, 123 | AuthInfo: clientcmdapi.AuthInfo{ 124 | AuthProvider: &clientcmdapi.AuthProviderConfig{ 125 | Name: "oidc", 126 | Config: map[string]string{ 127 | "client-id": cfg.ClientID, 128 | "client-secret": cfg.ClientSecret, 129 | "id-token": cfg.IDToken, 130 | "idp-issuer-url": cfg.IssuerURL, 131 | "refresh-token": cfg.RefreshToken, 132 | }, 133 | }, 134 | }, 135 | }, 136 | }, 137 | } 138 | return kcfg 139 | } 140 | 141 | func loginRequired(next http.Handler) http.Handler { 142 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 143 | session, err := gangwayUserSession.Session.Get(r, "gangway_id_token") 144 | if err != nil { 145 | http.Redirect(w, r, cfg.GetRootPathPrefix(), http.StatusTemporaryRedirect) 146 | return 147 | } 148 | 149 | if session.Values["id_token"] == nil { 150 | http.Redirect(w, r, cfg.GetRootPathPrefix(), http.StatusTemporaryRedirect) 151 | return 152 | } 153 | 154 | next.ServeHTTP(w, r) 155 | }) 156 | } 157 | 158 | func homeHandler(w http.ResponseWriter, r *http.Request) { 159 | data := &homeInfo{ 160 | HTTPPath: cfg.HTTPPath, 161 | } 162 | 163 | serveTemplate("home.tmpl", data, w) 164 | } 165 | 166 | func loginHandler(w http.ResponseWriter, r *http.Request) { 167 | 168 | b := make([]byte, 32) 169 | rand.Read(b) 170 | state := url.QueryEscape(base64.StdEncoding.EncodeToString(b)) 171 | 172 | session, err := gangwayUserSession.Session.Get(r, "gangway") 173 | if err != nil { 174 | log.Errorf("Got an error in login: %s", err) 175 | http.Error(w, err.Error(), http.StatusInternalServerError) 176 | return 177 | } 178 | 179 | session.Values["state"] = state 180 | err = session.Save(r, w) 181 | if err != nil { 182 | http.Error(w, err.Error(), http.StatusInternalServerError) 183 | return 184 | } 185 | 186 | audience := oauth2.SetAuthURLParam("audience", cfg.Audience) 187 | url := oauth2Cfg.AuthCodeURL(state, audience) 188 | 189 | http.Redirect(w, r, url, http.StatusTemporaryRedirect) 190 | } 191 | 192 | func logoutHandler(w http.ResponseWriter, r *http.Request) { 193 | gangwayUserSession.Cleanup(w, r, "gangway") 194 | gangwayUserSession.Cleanup(w, r, "gangway_id_token") 195 | gangwayUserSession.Cleanup(w, r, "gangway_refresh_token") 196 | http.Redirect(w, r, cfg.GetRootPathPrefix(), http.StatusTemporaryRedirect) 197 | } 198 | 199 | func callbackHandler(w http.ResponseWriter, r *http.Request) { 200 | ctx := context.WithValue(r.Context(), oauth2.HTTPClient, transportConfig.HTTPClient) 201 | 202 | // load up session cookies 203 | session, err := gangwayUserSession.Session.Get(r, "gangway") 204 | if err != nil { 205 | http.Error(w, err.Error(), http.StatusInternalServerError) 206 | return 207 | } 208 | 209 | sessionIDToken, err := gangwayUserSession.Session.Get(r, "gangway_id_token") 210 | if err != nil { 211 | http.Error(w, err.Error(), http.StatusInternalServerError) 212 | return 213 | } 214 | 215 | sessionRefreshToken, err := gangwayUserSession.Session.Get(r, "gangway_refresh_token") 216 | if err != nil { 217 | http.Error(w, err.Error(), http.StatusInternalServerError) 218 | return 219 | } 220 | 221 | // verify the state string 222 | state := r.URL.Query().Get("state") 223 | 224 | if state != session.Values["state"] { 225 | http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) 226 | return 227 | } 228 | 229 | // use the access code to retrieve a token 230 | code := r.URL.Query().Get("code") 231 | token, err := o2token.Exchange(ctx, code) 232 | if err != nil { 233 | http.Error(w, err.Error(), http.StatusInternalServerError) 234 | return 235 | } 236 | 237 | sessionIDToken.Values["id_token"] = token.Extra("id_token") 238 | sessionRefreshToken.Values["refresh_token"] = token.RefreshToken 239 | 240 | // save the session cookies 241 | err = session.Save(r, w) 242 | if err != nil { 243 | http.Error(w, err.Error(), http.StatusInternalServerError) 244 | return 245 | } 246 | err = sessionIDToken.Save(r, w) 247 | if err != nil { 248 | http.Error(w, err.Error(), http.StatusInternalServerError) 249 | return 250 | } 251 | err = sessionRefreshToken.Save(r, w) 252 | if err != nil { 253 | http.Error(w, err.Error(), http.StatusInternalServerError) 254 | return 255 | } 256 | 257 | http.Redirect(w, r, fmt.Sprintf("%s/commandline", cfg.HTTPPath), http.StatusSeeOther) 258 | } 259 | 260 | func commandlineHandler(w http.ResponseWriter, r *http.Request) { 261 | info := generateInfo(w, r) 262 | if info == nil { 263 | // generateInfo writes to the ResponseWriter if it encounters an error. 264 | // TODO(abrand): Refactor this. 265 | return 266 | } 267 | 268 | serveTemplate("commandline.tmpl", info, w) 269 | } 270 | 271 | func kubeConfigHandler(w http.ResponseWriter, r *http.Request) { 272 | info := generateInfo(w, r) 273 | if info == nil { 274 | // generateInfo writes to the ResponseWriter if it encounters an error. 275 | // TODO(abrand): Refactor this. 276 | return 277 | } 278 | 279 | d, err := yaml.Marshal(generateKubeConfig(info)) 280 | if err != nil { 281 | log.Errorf("Error creating kubeconfig - %s", err.Error()) 282 | http.Error(w, "Error creating kubeconfig", http.StatusInternalServerError) 283 | return 284 | } 285 | 286 | // tell the browser the returned content should be downloaded 287 | w.Header().Add("Content-Disposition", "Attachment") 288 | w.Write(d) 289 | } 290 | 291 | func generateInfo(w http.ResponseWriter, r *http.Request) *userInfo { 292 | // read in public ca.crt to output in commandline copy/paste commands 293 | file, err := os.Open(cfg.ClusterCAPath) 294 | if err != nil { 295 | // let us know that we couldn't open the file. This only cause missing output 296 | // does not impact actual function of program 297 | log.Errorf("Failed to open CA file. %s", err) 298 | } 299 | defer file.Close() 300 | caBytes, err := ioutil.ReadAll(file) 301 | if err != nil { 302 | log.Warningf("Could not read CA file: %s", err) 303 | } 304 | 305 | // load the session cookies 306 | sessionIDToken, err := gangwayUserSession.Session.Get(r, "gangway_id_token") 307 | if err != nil { 308 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 309 | return nil 310 | } 311 | sessionRefreshToken, err := gangwayUserSession.Session.Get(r, "gangway_refresh_token") 312 | if err != nil { 313 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 314 | return nil 315 | } 316 | 317 | idToken, ok := sessionIDToken.Values["id_token"].(string) 318 | if !ok { 319 | gangwayUserSession.Cleanup(w, r, "gangway") 320 | gangwayUserSession.Cleanup(w, r, "gangway_id_token") 321 | gangwayUserSession.Cleanup(w, r, "gangway_refresh_token") 322 | 323 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect) 324 | return nil 325 | } 326 | 327 | refreshToken, ok := sessionRefreshToken.Values["refresh_token"].(string) 328 | if !ok { 329 | gangwayUserSession.Cleanup(w, r, "gangway") 330 | gangwayUserSession.Cleanup(w, r, "gangway_id_token") 331 | gangwayUserSession.Cleanup(w, r, "gangway_refresh_token") 332 | 333 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect) 334 | return nil 335 | } 336 | 337 | jwtToken, err := oidc.ParseToken(idToken, cfg.ClientSecret) 338 | if err != nil { 339 | http.Error(w, "Could not parse JWT", http.StatusInternalServerError) 340 | return nil 341 | } 342 | 343 | claims := jwtToken.Claims.(jwt.MapClaims) 344 | username, ok := claims[cfg.UsernameClaim].(string) 345 | if !ok { 346 | http.Error(w, "Could not parse Username claim", http.StatusInternalServerError) 347 | return nil 348 | } 349 | 350 | kubeCfgUser := strings.Join([]string{username, cfg.ClusterName}, "@") 351 | 352 | if cfg.EmailClaim != "" { 353 | log.Warn("using the Email Claim config setting is deprecated. Gangway uses `UsernameClaim@ClusterName`. This field will be removed in a future version.") 354 | } 355 | 356 | issuerURL, ok := claims["iss"].(string) 357 | if !ok { 358 | http.Error(w, "Could not parse Issuer URL claim", http.StatusInternalServerError) 359 | return nil 360 | } 361 | 362 | if cfg.ClientSecret == "" { 363 | log.Warn("Setting an empty Client Secret should only be done if you have no other option and is an inherent security risk.") 364 | } 365 | 366 | info := &userInfo{ 367 | ClusterName: cfg.ClusterName, 368 | Username: username, 369 | Claims: claims, 370 | KubeCfgUser: kubeCfgUser, 371 | IDToken: idToken, 372 | RefreshToken: refreshToken, 373 | ClientID: cfg.ClientID, 374 | ClientSecret: cfg.ClientSecret, 375 | IssuerURL: issuerURL, 376 | APIServerURL: cfg.APIServerURL, 377 | ClusterCA: string(caBytes), 378 | HTTPPath: cfg.HTTPPath, 379 | } 380 | return info 381 | } 382 | -------------------------------------------------------------------------------- /cmd/gangway/handlers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Heptio 2 | // Copyright © 2017 Craig Tracey 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "io/ioutil" 22 | "net/http" 23 | "net/http/httptest" 24 | "reflect" 25 | "regexp" 26 | "strings" 27 | "testing" 28 | 29 | "github.com/ghodss/yaml" 30 | 31 | "github.com/gorilla/sessions" 32 | "github.com/heptiolabs/gangway/internal/config" 33 | "github.com/heptiolabs/gangway/internal/session" 34 | "golang.org/x/oauth2" 35 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api/v1" 36 | ) 37 | 38 | func testInit() { 39 | gangwayUserSession = session.New("test") 40 | transportConfig = config.NewTransportConfig("") 41 | 42 | oauth2Cfg = &oauth2.Config{ 43 | ClientID: "cfg.ClientID", 44 | ClientSecret: "qwertyuiopasdfghjklzxcvbnm123456", 45 | RedirectURL: "cfg.RedirectURL", 46 | } 47 | 48 | o2token = &FakeToken{ 49 | OAuth2Cfg: oauth2Cfg, 50 | } 51 | } 52 | 53 | func TestHomeHandler(t *testing.T) { 54 | req, err := http.NewRequest("GET", "/", nil) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | cfg = &config.Config{ 60 | HTTPPath: "", 61 | } 62 | 63 | rr := httptest.NewRecorder() 64 | handler := http.HandlerFunc(homeHandler) 65 | 66 | handler.ServeHTTP(rr, req) 67 | if status := rr.Code; status != http.StatusOK { 68 | t.Errorf("handler returned wrong status code: got %v want %v", 69 | status, http.StatusOK) 70 | } 71 | } 72 | 73 | func TestCallbackHandler(t *testing.T) { 74 | tests := map[string]struct { 75 | params map[string]string 76 | expectedStatusCode int 77 | }{ 78 | "default": { 79 | params: map[string]string{ 80 | "state": "Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=", 81 | "code": "0cj0VQzNl36e4P2L&state=jdep4ov52FeUuzWLDDtSXaF4b5%2F%2FCUJ52xlE69ehnQ8%3D", 82 | }, 83 | expectedStatusCode: http.StatusSeeOther, 84 | }, 85 | } 86 | 87 | for name, tc := range tests { 88 | t.Run(name, func(t *testing.T) { 89 | var req *http.Request 90 | var rsp *httptest.ResponseRecorder 91 | var session *sessions.Session 92 | var err error 93 | 94 | cfg = &config.Config{ 95 | HTTPPath: "/foo", 96 | } 97 | 98 | // Init variables 99 | rsp = NewRecorder() 100 | testInit() 101 | req, err = http.NewRequest("GET", "/callback", nil) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | 106 | // Create request 107 | if session, err = gangwayUserSession.Session.Get(req, "gangway"); err != nil { 108 | t.Fatalf("Error getting session: %v", err) 109 | } 110 | 111 | // Create state session variable 112 | session.Values["state"] = tc.params["state"] 113 | if err = session.Save(req, rsp); err != nil { 114 | t.Fatal(err) 115 | } 116 | 117 | // Add query params to request 118 | q := req.URL.Query() 119 | for k, v := range tc.params { 120 | q.Add(k, v) 121 | } 122 | req.URL.RawQuery = q.Encode() 123 | 124 | handler := http.HandlerFunc(callbackHandler) 125 | 126 | // Call Handler 127 | handler.ServeHTTP(rsp, req) 128 | 129 | // Validate! 130 | if status := rsp.Code; status != tc.expectedStatusCode { 131 | t.Errorf("handler returned wrong status code: got %v want %v", status, tc.expectedStatusCode) 132 | } 133 | 134 | }) 135 | } 136 | 137 | } 138 | func TestCommandLineHandler(t *testing.T) { 139 | tests := map[string]struct { 140 | params map[string]string 141 | emailClaim string 142 | usernameClaim string 143 | expectedStatusCode int 144 | expectedUsernameInTemplate string 145 | }{ 146 | "default": { 147 | params: map[string]string{ 148 | "state": "Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=", 149 | "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJHYW5nd2F5VGVzdCIsImlhdCI6MTU0MDA0NjM0NywiZXhwIjoxODg3MjAxNTQ3LCJhdWQiOiJnYW5nd2F5LmhlcHRpby5jb20iLCJzdWIiOiJnYW5nd2F5QGhlcHRpby5jb20iLCJHaXZlbk5hbWUiOiJHYW5nIiwiU3VybmFtZSI6IldheSIsIkVtYWlsIjoiZ2FuZ3dheUBoZXB0aW8uY29tIiwiR3JvdXBzIjoiZGV2LGFkbWluIn0.zNG4Dnxr76J0p4phfsAUYWunioct0krkMiunMynlQsU", 150 | "refresh_token": "bar", 151 | "code": "0cj0VQzNl36e4P2L&state=jdep4ov52FeUuzWLDDtSXaF4b5%2F%2FCUJ52xlE69ehnQ8%3D", 152 | }, 153 | expectedStatusCode: http.StatusOK, 154 | expectedUsernameInTemplate: "gangway@heptio.com", 155 | emailClaim: "Email", 156 | usernameClaim: "sub", 157 | }, 158 | "incorrect username claim": { 159 | params: map[string]string{ 160 | "state": "Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=", 161 | "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJHYW5nd2F5VGVzdCIsImlhdCI6MTU0MDA0NjM0NywiZXhwIjoxODg3MjAxNTQ3LCJhdWQiOiJnYW5nd2F5LmhlcHRpby5jb20iLCJzdWIiOiJnYW5nd2F5QGhlcHRpby5jb20iLCJHaXZlbk5hbWUiOiJHYW5nIiwiU3VybmFtZSI6IldheSIsIkVtYWlsIjoiZ2FuZ3dheUBoZXB0aW8uY29tIiwiR3JvdXBzIjoiZGV2LGFkbWluIn0.zNG4Dnxr76J0p4phfsAUYWunioct0krkMiunMynlQsU", 162 | "refresh_token": "bar", 163 | "code": "0cj0VQzNl36e4P2L&state=jdep4ov52FeUuzWLDDtSXaF4b5%2F%2FCUJ52xlE69ehnQ8%3D", 164 | }, 165 | expectedStatusCode: http.StatusInternalServerError, 166 | emailClaim: "Email", 167 | usernameClaim: "meh", 168 | }, 169 | "no email claim": { 170 | params: map[string]string{ 171 | "state": "Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=", 172 | "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJHYW5nd2F5VGVzdCIsImlhdCI6MTU0MDA0NjM0NywiZXhwIjoxODg3MjAxNTQ3LCJhdWQiOiJnYW5nd2F5LmhlcHRpby5jb20iLCJzdWIiOiJnYW5nd2F5QGhlcHRpby5jb20iLCJHaXZlbk5hbWUiOiJHYW5nIiwiU3VybmFtZSI6IldheSIsIkVtYWlsIjoiZ2FuZ3dheUBoZXB0aW8uY29tIiwiR3JvdXBzIjoiZGV2LGFkbWluIn0.zNG4Dnxr76J0p4phfsAUYWunioct0krkMiunMynlQsU", 173 | "refresh_token": "bar", 174 | "code": "0cj0VQzNl36e4P2L&state=jdep4ov52FeUuzWLDDtSXaF4b5%2F%2FCUJ52xlE69ehnQ8%3D", 175 | }, 176 | expectedStatusCode: http.StatusOK, 177 | expectedUsernameInTemplate: "gangway@heptio.com@cluster1", 178 | usernameClaim: "sub", 179 | }, 180 | } 181 | 182 | for name, tc := range tests { 183 | t.Run(name, func(t *testing.T) { 184 | var req *http.Request 185 | var rsp *httptest.ResponseRecorder 186 | var session *sessions.Session 187 | var sessionIDToken *sessions.Session 188 | var sessionRefreshToken *sessions.Session 189 | var err error 190 | 191 | cfg = &config.Config{ 192 | HTTPPath: "/foo", 193 | EmailClaim: tc.emailClaim, 194 | UsernameClaim: tc.usernameClaim, 195 | ClusterName: "cluster1", 196 | APIServerURL: "https://kubernetes", 197 | } 198 | 199 | // Init variables 200 | rsp = NewRecorder() 201 | testInit() 202 | req, err = http.NewRequest("GET", "/callback", nil) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | 207 | // Create request 208 | if session, err = gangwayUserSession.Session.Get(req, "gangway"); err != nil { 209 | t.Fatalf("Error getting session: %v", err) 210 | } 211 | if sessionIDToken, err = gangwayUserSession.Session.Get(req, "gangway_id_token"); err != nil { 212 | t.Fatalf("Error getting session: %v", err) 213 | } 214 | if sessionRefreshToken, err = gangwayUserSession.Session.Get(req, "gangway_refresh_token"); err != nil { 215 | t.Fatalf("Error getting session: %v", err) 216 | } 217 | 218 | // Create state session variable 219 | session.Values["state"] = tc.params["state"] 220 | sessionIDToken.Values["id_token"] = tc.params["id_token"] 221 | sessionRefreshToken.Values["refresh_token"] = tc.params["refresh_token"] 222 | if err = session.Save(req, rsp); err != nil { 223 | t.Fatal(err) 224 | } 225 | if err = sessionIDToken.Save(req, rsp); err != nil { 226 | t.Fatal(err) 227 | } 228 | if err = sessionRefreshToken.Save(req, rsp); err != nil { 229 | t.Fatal(err) 230 | } 231 | 232 | // Add query params to request 233 | q := req.URL.Query() 234 | for k, v := range tc.params { 235 | q.Add(k, v) 236 | } 237 | req.URL.RawQuery = q.Encode() 238 | 239 | handler := http.HandlerFunc(commandlineHandler) 240 | 241 | // Call Handler 242 | handler.ServeHTTP(rsp, req) 243 | 244 | // Validate! 245 | if status := rsp.Code; status != tc.expectedStatusCode { 246 | t.Errorf("handler returned wrong status code: got %v want %v", status, tc.expectedStatusCode) 247 | } 248 | // if response code is OK then check that username is correct in resultant template 249 | if rsp.Code == 200 { 250 | bodyBytes, _ := ioutil.ReadAll(rsp.Body) 251 | bodyString := string(bodyBytes) 252 | re := regexp.MustCompile("--user=(.+)") 253 | found := re.FindString(bodyString) 254 | if !strings.Contains(found, tc.expectedUsernameInTemplate) { 255 | t.Errorf("template should contain --user=%s but found %s", tc.expectedUsernameInTemplate, found) 256 | } 257 | } 258 | }) 259 | } 260 | } 261 | 262 | func TestKubeconfigHandler(t *testing.T) { 263 | tests := map[string]struct { 264 | cfg config.Config 265 | params map[string]string 266 | usernameClaim string 267 | expectedStatusCode int 268 | expectedAuthInfoName string 269 | expectedAuthInfoAuthProviderConfig map[string]string 270 | }{ 271 | "default": { 272 | cfg: config.Config{ 273 | UsernameClaim: "sub", 274 | ClusterName: "cluster1", 275 | APIServerURL: "https://kubernetes", 276 | ClientID: "someClientID", 277 | ClientSecret: "someClientSecret", 278 | }, 279 | params: map[string]string{ 280 | "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJHYW5nd2F5VGVzdCIsImlhdCI6MTU0MDA0NjM0NywiZXhwIjoxODg3MjAxNTQ3LCJhdWQiOiJnYW5nd2F5LmhlcHRpby5jb20iLCJzdWIiOiJnYW5nd2F5QGhlcHRpby5jb20iLCJHaXZlbk5hbWUiOiJHYW5nIiwiU3VybmFtZSI6IldheSIsIkVtYWlsIjoiZ2FuZ3dheUBoZXB0aW8uY29tIiwiR3JvdXBzIjoiZGV2LGFkbWluIn0.zNG4Dnxr76J0p4phfsAUYWunioct0krkMiunMynlQsU", 281 | "refresh_token": "bar", 282 | }, 283 | expectedStatusCode: http.StatusOK, 284 | usernameClaim: "sub", 285 | expectedAuthInfoName: "gangway@heptio.com@cluster1", 286 | expectedAuthInfoAuthProviderConfig: map[string]string{ 287 | "client-id": "someClientID", 288 | "client-secret": "someClientSecret", 289 | "id-token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJHYW5nd2F5VGVzdCIsImlhdCI6MTU0MDA0NjM0NywiZXhwIjoxODg3MjAxNTQ3LCJhdWQiOiJnYW5nd2F5LmhlcHRpby5jb20iLCJzdWIiOiJnYW5nd2F5QGhlcHRpby5jb20iLCJHaXZlbk5hbWUiOiJHYW5nIiwiU3VybmFtZSI6IldheSIsIkVtYWlsIjoiZ2FuZ3dheUBoZXB0aW8uY29tIiwiR3JvdXBzIjoiZGV2LGFkbWluIn0.zNG4Dnxr76J0p4phfsAUYWunioct0krkMiunMynlQsU", 290 | "refresh-token": "bar", 291 | "idp-issuer-url": "GangwayTest", 292 | }, 293 | }, 294 | } 295 | 296 | for name, tc := range tests { 297 | t.Run(name, func(t *testing.T) { 298 | var req *http.Request 299 | var rsp *httptest.ResponseRecorder 300 | var session *sessions.Session 301 | var sessionIDToken *sessions.Session 302 | var sessionRefreshToken *sessions.Session 303 | var err error 304 | 305 | // Create dummy cluster CA file 306 | clusterCAData := "dummy cluster CA" 307 | f, err := ioutil.TempFile("", "gangway-kubeconfig-handler-test") 308 | if err != nil { 309 | t.Fatalf("Error creating temp file: %v", err) 310 | } 311 | fmt.Fprint(f, clusterCAData) 312 | 313 | // Set config global var 314 | cfg = &tc.cfg 315 | cfg.ClusterCAPath = f.Name() 316 | 317 | // Init variables 318 | rsp = NewRecorder() 319 | testInit() 320 | req, err = http.NewRequest("GET", "/kubeconf", nil) 321 | if err != nil { 322 | t.Fatal(err) 323 | } 324 | 325 | // Create request 326 | if session, err = gangwayUserSession.Session.Get(req, "gangway"); err != nil { 327 | t.Fatalf("Error getting session: %v", err) 328 | } 329 | if sessionIDToken, err = gangwayUserSession.Session.Get(req, "gangway_id_token"); err != nil { 330 | t.Fatalf("Error getting session: %v", err) 331 | } 332 | if sessionRefreshToken, err = gangwayUserSession.Session.Get(req, "gangway_refresh_token"); err != nil { 333 | t.Fatalf("Error getting session: %v", err) 334 | } 335 | 336 | sessionIDToken.Values["id_token"] = tc.params["id_token"] 337 | sessionRefreshToken.Values["refresh_token"] = tc.params["refresh_token"] 338 | if err = session.Save(req, rsp); err != nil { 339 | t.Fatal(err) 340 | } 341 | if err = sessionIDToken.Save(req, rsp); err != nil { 342 | t.Fatal(err) 343 | } 344 | if err = sessionRefreshToken.Save(req, rsp); err != nil { 345 | t.Fatal(err) 346 | } 347 | 348 | // Add query params to request 349 | q := req.URL.Query() 350 | for k, v := range tc.params { 351 | q.Add(k, v) 352 | } 353 | req.URL.RawQuery = q.Encode() 354 | 355 | handler := http.HandlerFunc(kubeConfigHandler) 356 | 357 | // Call Handler 358 | handler.ServeHTTP(rsp, req) 359 | 360 | // Validate 361 | if status := rsp.Code; status != tc.expectedStatusCode { 362 | t.Errorf("handler returned wrong status code: got %v want %v", status, tc.expectedStatusCode) 363 | } 364 | // if response code is OK, validate the kubeconfig 365 | if rsp.Code == 200 { 366 | bodyBytes, err := ioutil.ReadAll(rsp.Body) 367 | if err != nil { 368 | t.Fatalf("error reading body: %v", err) 369 | } 370 | kubeconfig := &clientcmdapi.Config{} 371 | if err := yaml.Unmarshal(bodyBytes, kubeconfig); err != nil { 372 | t.Fatalf("error unmarshaling response: %v", err) 373 | } 374 | 375 | // Validate cluster 376 | if len(kubeconfig.Clusters) != 1 { 377 | t.Fatalf("Found %d clusters in the generated kubeconfig, expected 1", len(kubeconfig.Clusters)) 378 | } 379 | cluster := kubeconfig.Clusters[0] 380 | if cluster.Name != cfg.ClusterName { 381 | t.Errorf("Expected cluster name to be %q, but found %q", cfg.ClusterName, kubeconfig.Clusters[0].Name) 382 | } 383 | if cluster.Cluster.Server != cfg.APIServerURL { 384 | t.Errorf("Expected cluster server to be %q, but found %q", cfg.APIServerURL, cluster.Cluster.Server) 385 | } 386 | if string(cluster.Cluster.CertificateAuthorityData) != clusterCAData { 387 | t.Errorf("Expected cluster CA Data %q, but got %q", clusterCAData, string(cluster.Cluster.CertificateAuthorityData)) 388 | } 389 | 390 | // Validate AuthInfo 391 | if len(kubeconfig.AuthInfos) != 1 { 392 | t.Fatalf("Found %d users in the generated kubeconfig, expected 1", len(kubeconfig.AuthInfos)) 393 | } 394 | authInfo := kubeconfig.AuthInfos[0] 395 | if authInfo.Name != tc.expectedAuthInfoName { 396 | t.Errorf("Expected AuthInfo.Name %q, but got %q", tc.expectedAuthInfoName, authInfo.Name) 397 | } 398 | 399 | if authInfo.AuthInfo.AuthProvider.Name != "oidc" { 400 | t.Errorf("expecetd authprovider to be oidc, got %s", authInfo.AuthInfo.AuthProvider.Name) 401 | } 402 | if !reflect.DeepEqual(authInfo.AuthInfo.AuthProvider.Config, tc.expectedAuthInfoAuthProviderConfig) { 403 | t.Errorf("Expected %v, got %v", tc.expectedAuthInfoAuthProviderConfig, authInfo.AuthInfo.AuthProvider.Config) 404 | } 405 | 406 | // Validate context 407 | if len(kubeconfig.Contexts) != 1 { 408 | t.Fatalf("Found %d contexts in the generated kubeconfig, expected 1", len(kubeconfig.Contexts)) 409 | } 410 | context := kubeconfig.Contexts[0] 411 | if context.Name != cfg.ClusterName { 412 | t.Errorf("Expected context name to be %q, but found %q", cfg.ClusterName, context.Name) 413 | } 414 | if context.Context.Cluster != cluster.Name { 415 | t.Errorf("Cluster name %q in context does not match cluster name %q", context.Context.Cluster, cluster.Name) 416 | } 417 | if context.Context.AuthInfo != authInfo.Name { 418 | t.Errorf("AuthInfo name %q in context does not match user name %q", context.Context.AuthInfo, authInfo.Name) 419 | } 420 | if kubeconfig.CurrentContext != context.Name { 421 | t.Errorf("Current context %q does not match context name %q", kubeconfig.CurrentContext, context.Name) 422 | } 423 | } 424 | }) 425 | } 426 | } 427 | 428 | func TestUnauthedCommandlineHandlerRedirect(t *testing.T) { 429 | testInit() 430 | 431 | req, err := http.NewRequest("GET", "/commandline", nil) 432 | if err != nil { 433 | t.Fatal(err) 434 | } 435 | 436 | session.New("test") 437 | 438 | rr := httptest.NewRecorder() 439 | handler := http.HandlerFunc(commandlineHandler) 440 | 441 | handler.ServeHTTP(rr, req) 442 | if status := rr.Code; status != http.StatusTemporaryRedirect { 443 | t.Errorf("handler returned wrong status code: got %v want %v", 444 | status, http.StatusOK) 445 | } 446 | } 447 | 448 | // NewRecorder returns an initialized ResponseRecorder. 449 | func NewRecorder() *httptest.ResponseRecorder { 450 | return &httptest.ResponseRecorder{ 451 | HeaderMap: make(http.Header), 452 | Body: new(bytes.Buffer), 453 | } 454 | } 455 | 456 | type FakeToken struct { 457 | OAuth2Cfg *oauth2.Config 458 | } 459 | 460 | // Exchange takes an oauth2 auth token and exchanges for an id_token 461 | func (f *FakeToken) Exchange(ctx context.Context, code string) (*oauth2.Token, error) { 462 | return &oauth2.Token{ 463 | AccessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJHYW5nd2F5VGVzdCIsImlhdCI6MTU0MDA0NjM0NywiZXhwIjoxODg3MjAxNTQ3LCJhdWQiOiJnYW5nd2F5LmhlcHRpby5jb20iLCJzdWIiOiJnYW5nd2F5QGhlcHRpby5jb20iLCJHaXZlbk5hbWUiOiJHYW5nIiwiU3VybmFtZSI6IldheSIsIkVtYWlsIjoiZ2FuZ3dheUBoZXB0aW8uY29tIiwiR3JvdXBzIjoiZGV2LGFkbWluIn0.zNG4Dnxr76J0p4phfsAUYWunioct0krkMiunMynlQsU", 464 | RefreshToken: "4567", 465 | }, nil 466 | } 467 | -------------------------------------------------------------------------------- /cmd/gangway/main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Heptio 2 | // Copyright © 2017 Craig Tracey 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "crypto/tls" 20 | "flag" 21 | "fmt" 22 | "net/http" 23 | "os" 24 | "os/signal" 25 | "syscall" 26 | "time" 27 | 28 | "github.com/heptiolabs/gangway/internal/config" 29 | "github.com/heptiolabs/gangway/internal/oidc" 30 | "github.com/heptiolabs/gangway/internal/session" 31 | "github.com/justinas/alice" 32 | log "github.com/sirupsen/logrus" 33 | "golang.org/x/oauth2" 34 | ) 35 | 36 | var cfg *config.Config 37 | var oauth2Cfg *oauth2.Config 38 | var o2token oidc.OAuth2Token 39 | var gangwayUserSession *session.Session 40 | var transportConfig *config.TransportConfig 41 | 42 | // wrapper function for http logging 43 | func httpLogger(fn http.HandlerFunc) http.HandlerFunc { 44 | return func(w http.ResponseWriter, r *http.Request) { 45 | defer log.Printf("%s %s %s", r.Method, r.URL, r.RemoteAddr) 46 | fn(w, r) 47 | } 48 | } 49 | 50 | func main() { 51 | cfgFile := flag.String("config", "", "The config file to use.") 52 | flag.Parse() 53 | 54 | var err error 55 | cfg, err = config.NewConfig(*cfgFile) 56 | if err != nil { 57 | log.Errorf("Could not parse config file: %s", err) 58 | os.Exit(1) 59 | } 60 | 61 | oauth2Cfg = &oauth2.Config{ 62 | ClientID: cfg.ClientID, 63 | ClientSecret: cfg.ClientSecret, 64 | RedirectURL: cfg.RedirectURL, 65 | Scopes: cfg.Scopes, 66 | Endpoint: oauth2.Endpoint{ 67 | AuthURL: cfg.AuthorizeURL, 68 | TokenURL: cfg.TokenURL, 69 | }, 70 | } 71 | 72 | o2token = &oidc.Token{ 73 | OAuth2Cfg: oauth2Cfg, 74 | } 75 | 76 | transportConfig = config.NewTransportConfig(cfg.TrustedCAPath) 77 | gangwayUserSession = session.New(cfg.SessionSecurityKey) 78 | 79 | loginRequiredHandlers := alice.New(loginRequired) 80 | 81 | http.HandleFunc(cfg.GetRootPathPrefix(), httpLogger(homeHandler)) 82 | http.HandleFunc(fmt.Sprintf("%s/login", cfg.HTTPPath), httpLogger(loginHandler)) 83 | http.HandleFunc(fmt.Sprintf("%s/callback", cfg.HTTPPath), httpLogger(callbackHandler)) 84 | 85 | // middleware'd routes 86 | http.Handle(fmt.Sprintf("%s/logout", cfg.HTTPPath), loginRequiredHandlers.ThenFunc(logoutHandler)) 87 | http.Handle(fmt.Sprintf("%s/commandline", cfg.HTTPPath), loginRequiredHandlers.ThenFunc(commandlineHandler)) 88 | http.Handle(fmt.Sprintf("%s/kubeconf", cfg.HTTPPath), loginRequiredHandlers.ThenFunc(kubeConfigHandler)) 89 | 90 | bindAddr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) 91 | // create http server with timeouts 92 | httpServer := &http.Server{ 93 | Addr: bindAddr, 94 | ReadTimeout: 10 * time.Second, 95 | WriteTimeout: 10 * time.Second, 96 | } 97 | 98 | if cfg.ServeTLS { 99 | // update http server with TLS config 100 | httpServer.TLSConfig = &tls.Config{ 101 | CipherSuites: []uint16{ 102 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 103 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 104 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 105 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 106 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 107 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 108 | tls.TLS_RSA_WITH_AES_128_GCM_SHA256, 109 | tls.TLS_RSA_WITH_AES_256_GCM_SHA384, 110 | }, 111 | PreferServerCipherSuites: true, 112 | MinVersion: tls.VersionTLS12, 113 | } 114 | } 115 | 116 | // start up the http server 117 | go func() { 118 | log.Infof("Gangway started! Listening on: %s", bindAddr) 119 | 120 | // exit with FATAL logging why we could not start 121 | // example: FATA[0000] listen tcp 0.0.0.0:8080: bind: address already in use 122 | if cfg.ServeTLS { 123 | log.Fatal(httpServer.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile)) 124 | } else { 125 | log.Fatal(httpServer.ListenAndServe()) 126 | } 127 | }() 128 | 129 | // create channel listening for signals so we can have graceful shutdowns 130 | signalChan := make(chan os.Signal, 1) 131 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 132 | <-signalChan 133 | 134 | log.Println("Shutdown signal received, exiting.") 135 | // close the HTTP server 136 | httpServer.Shutdown(context.Background()) 137 | } 138 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Deploying Gangway 2 | 3 | Deploying Gangway consists of writing a config file and then deploying the service. 4 | The service is stateless so it is relatively easy to manage on Kubernetes. 5 | How you provide access to the service is going to be dependent on your specific configuration. 6 | 7 | A good starting point for yaml manifests for deploying to Kubernetes is in the [yaml](./yaml) directory. 8 | This creates a namespace, configmap, deployment and service. 9 | There is also an example ingress config that is set up to work with [Project Contour](https://github.com/projectcontour/contour), [JetStack cert-manager](https://github.com/jetstack/cert-manager) and [Let's Encrypt](https://letsencrypt.org/). 10 | See [this guide](https://projectcontour.io/guides/cert-manager/) for help getting started. 11 | 12 | You will probably have to adjust the service and ingress configs to match your environment as there is no one true way to reach services in Kubernetes that will work for everyone. 13 | 14 | The "client secret" is embedded in this config file. 15 | While this is called a secret, based on the way that OAuth2 works with command line tools, this secret won't be all secret. 16 | This will be divulged to any client that is configured through gangway. 17 | As such, it is probably acceptable to keep that secret in the config file and not worry about managing it as a true secret. 18 | 19 | We also have a secret string that is used to as a way to encrypt the cookies that are returned to the users. 20 | If using the example YAML, create a secret to hold this value with the following command line: 21 | 22 | ``` 23 | kubectl -n gangway create secret generic gangway-key \ 24 | --from-literal=sessionkey=$(openssl rand -base64 32) 25 | ``` 26 | 27 | ## Path Prefix 28 | 29 | Gangway takes an optional path prefix if you want to host it at a url other than '/' (e.g. `https://example.com/gangway`). 30 | By configuring this parameter, all redirects will have the proper path appended to the url parameters. 31 | 32 | This variable can be configured via the [ConfigMap](https://github.com/heptiolabs/gangway/blob/master/docs/yaml/02-config.yaml#L81) or via environment variable (`GANGWAY_HTTP_PATH`). 33 | 34 | ## Detailed Instructions 35 | 36 | The following guide is a more detailed review of how to get Gangway and other components configured in an AWS environment. 37 | AWS is not a requirement, rather, is just an example of how a fully functional system can be constructed and components used may vary depending on the specific environment that you are targeting. 38 | 39 | It's also important to note that this guide does not include an Auth provider, that comes in the next section: 40 | 41 | We will use the following components: 42 | 43 | - [gangway](https://github.com/heptiolabs/gangway): OIDC client application 44 | - [contour](https://github.com/projectcontour/contour): Kubernetes Ingress controller 45 | - [cert-manager](https://github.com/jetstack/cert-manager): Controller for managing TLS certificates with Let's Encrypt. 46 | 47 | ### Cert-Manager 48 | 49 | Run: 50 | 51 | ```sh 52 | kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/v0.4.1/contrib/manifests/cert-manager/with-rbac.yaml 53 | ``` 54 | 55 | Apply the following config to create the staging and production `ClusterIssuer` making sure to update the `email` field below with your email address: 56 | 57 | ```sh 58 | cat < 67 | http01: {} 68 | privateKeySecretRef: 69 | name: letsencrypt-prod 70 | server: https://acme-v02.api.letsencrypt.org/directory 71 | --- 72 | apiVersion: certmanager.k8s.io/v1alpha1 73 | kind: ClusterIssuer 74 | metadata: 75 | name: letsencrypt-staging 76 | namespace: cert-manager 77 | spec: 78 | acme: 79 | email: 80 | http01: {} 81 | privateKeySecretRef: 82 | name: letsencrypt-staging 83 | server: https://acme-staging-v02.api.letsencrypt.org/directory 84 | EOF 85 | ``` 86 | 87 | ### Contour 88 | 89 | Run: 90 | 91 | ```sh 92 | kubectl apply -f https://projectcontour.io/quickstart/contour.yaml 93 | ``` 94 | 95 | This will deploy Contour in the `projectcontour` namespace, and expose it using a service of type `LoadBalancer`. 96 | 97 | ### Configure DNS Record 98 | 99 | Get the hostname of the ELB that Kubernetes created for the contour service: 100 | 101 | ```sh 102 | kubectl get svc -n projectcontour contour -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 103 | ``` 104 | 105 | Create a wildcard CNAME record that aliases the domain under your control to the hostname of the ELB obtained above. 106 | 107 | For instance, if you own `example.com`, create a CNAME record for `*.example.com`, so that you can access gangway at `https://gangway.example.com`. 108 | 109 | ### Deploy Gangway 110 | 111 | #### Update config 112 | 113 | Update the following files before deploying replacing all env variables (e.g. ${VAR}): 114 | 115 | *Important: Each placeholder can show up multiple times in the same document. Make sure to update all occurrences.* 116 | 117 | - docs/yaml/05-ingress.yaml 118 | 119 | #### Deploy 120 | 121 | Once the placeholders have been updated, deploy: 122 | 123 | ```sh 124 | # This command will make sure that you have updated the placeholders 125 | grep -r '\${' docs/yaml/ || kubectl apply -f docs/yaml/01-namespace.yaml -f 03-deployment.yaml -f 04-service.yaml -f 05-ingress.yaml 126 | ``` 127 | 128 | #### Create secret 129 | 130 | Create the gangway cookies that are used to encrypt gangway cookies: 131 | 132 | ```sh 133 | kubectl -n gangway create secret generic gangway-key \ 134 | --from-literal=sessionkey=$(openssl rand -base64 32) 135 | ``` 136 | 137 | ### Validate Certs 138 | 139 | At this point Contour, Gangway and Cert-Manager should all be deployed. 140 | The default Ingress example included first uses the `staging` issuer for Let's Encrypt. 141 | This provisioner is intended to be used while the application is being setup and has no rate-limiting. 142 | If all is successful there should be a secret named `gangway` in the gangway namespace: 143 | 144 | ```sh 145 | $ kubectl describe secret gangway -n gangway 146 | Name: gangway 147 | Namespace: gangway 148 | Labels: certmanager.k8s.io/certificate-name=gangway 149 | Annotations: certmanager.k8s.io/alt-names=gangway.example.com 150 | certmanager.k8s.io/common-name=gangway.example.com 151 | certmanager.k8s.io/issuer-kind=ClusterIssuer 152 | certmanager.k8s.io/issuer-name=letsencrypt-staging 153 | 154 | Type: kubernetes.io/tls 155 | 156 | Data 157 | ==== 158 | tls.crt: 3818 bytes 159 | tls.key: 1675 bytes 160 | ``` 161 | 162 | To move to a real certificate, update the Ingress object `gangway` in the gangway namespace and change the annotation from `letsencrypt-staging` to `letsencrypt-prod`, then delete the gangway secret to have Cert-Manager request the new certificate. 163 | 164 | At this point there should be a valid certificate for gangway serving over TLS. 165 | Continue on to the next section to configure an Identity Provider. 166 | 167 | ## Identity Provider Configs 168 | 169 | Gangway can be used with a variety of OAuth2 identity providers. 170 | Here are some instructions for common ones. 171 | 172 | * [Auth0](auth0.md) 173 | * [Google](google.md) 174 | * [Dex](dex.md) 175 | 176 | ## Configure Role Binding 177 | 178 | Once Gangway is deployed and functional and the Identity Provider is functional you'll need to get a token. 179 | 180 | 1. Open up gangway and auth to your provider 181 | 2. Gangway will then return a command to run which will configure your kubectl locally 182 | 3. Configure RBAC permissions for the user. A simple example is included in this repo (`docs/yaml/role/rolebinding.yaml`). Update the user field and then apply which will make that user a `cluster-admin`. 183 | 184 | ## Docker image 185 | 186 | A recent release of Gangway is available at 187 | 188 | ``` 189 | gcr.io/heptio-images/gangway: 190 | ``` 191 | -------------------------------------------------------------------------------- /docs/auth0.md: -------------------------------------------------------------------------------- 1 | # Connecting Gangway to Auth0 2 | 3 | 1. Create an account for Auth0 and login 4 | 2. From the dashboard, click "New Application" 5 | 3. Enter a name and choose "Single Page Web Applications" 6 | 4. Click on "Settings" and gather the relevant information and update the file `docs/yaml/02-configmap.yaml` and apply to cluster 7 | 5. Update the "Allowed Callback URLs" to match the "redirectURL" parameter in the configmap configured previously 8 | 6. Click "Save Changes" 9 | 7. Add Rule for adding group metadata by clicking on "Rules" from the menu 10 | 8. Give the rule a name and copy/paste the following: 11 | 12 | ```go 13 | function (user, context, callback) { 14 | if (user.app_metadata && 'groups' in user.app_metadata) { 15 | context.idToken.groups = user.app_metadata.groups; 16 | } else { 17 | context.idToken.groups = []; 18 | } 19 | 20 | callback(null, user, context); 21 | } 22 | ``` 23 | 24 | 9. Configure API Server with the following config replacing issuer-url & client-id values: 25 | 26 | ``` 27 | --oidc-issuer-url=https://example.auth0.com/ 28 | --oidc-client-id= 29 | --oidc-username-claim=email 30 | --oidc-groups-claim=groups 31 | ``` 32 | 33 | ## Example 34 | 35 | A typical gangway config for Auth0: 36 | 37 | ```yaml 38 | clusterName: "YourCluster" 39 | authorizeURL: "https://example.auth0.com/authorize" 40 | tokenURL: "https://example.auth0.com/oauth/token" 41 | clientID: "" 42 | clientSecret: "" 43 | audience: "https://example.auth0.com/userinfo" 44 | redirectURL: "https://gangway.example.com/callback" 45 | scopes: ["openid", "profile", "email", "offline_access"] 46 | usernameClaim: "sub" 47 | emailClaim: "email" 48 | apiServerURL: "https://kube-apiserver.yourcluster.com" 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuring Gangway 2 | 3 | Gangway reads a configuration file on startup. The path to the configuration file must be set using the `--config` flag. 4 | 5 | The configuration file must be in YAML format, and contain a dictionary (aka. hash or map) of key/value pairs. The available options are described below. 6 | 7 | ## Configuration Options 8 | 9 | The following table describes the options that can be set via the YAML configuration file. 10 | 11 | | Key | Description | 12 | |------|----------------------------------------------------------------------------| 13 | | `host` | The address to listen on. Defaults to `0.0.0.0` (All interfaces). | 14 | | `port` | The port to listen on. Defaults to `8080`. | 15 | | `serveTLS` | Should Gangway serve TLS vs. plain HTTP? Defaults to `false`.| 16 | | `certFile` | The public cert file (including root and intermediates) to use when serving TLS. Defaults to `/etc/gangway/tls/tls.crt`. | 17 | | `keyFile` | The private key file when serving TLS. Defaults to `/etc/gangway/tls/tls.key`. | 18 | | `clusterName` | The cluster name. Used in the UI and kubectl config instructions | 19 | | `authorizeURL` | OAuth2 URL to start authorization flow.| 20 | | `tokenURL` | OAuth2 URL to obtain access tokens. | 21 | | `audience` | Endpoint that provides user profile information [optional]. Not all providers require this. | 22 | | `scopes` | Used to specify the scope of the requested Oauth authorization. Defaults to `["openid", "profile", "email", "offline_access"]` | 23 | | `redirectURL` | Where to redirect back to. This should be a URL where gangway is reachable. Typically this also needs to be registered as part of the oauth application with the oAuth provider. | 24 | | `clientID` | API client ID as indicated by the identity provider | 25 | | `clientSecret` | API client secret as indicated by the identity provider | 26 | | `allowEmptyClientSecret` | Some identity providers accept an empty client secret, this is not generally considered a good idea. If you have to use an empty secret and accept the risks that come with that then you can set this to true. Defaults to `false`. | 27 | | `usernameClaim` | The JWT claim to use as the username. This is used in UI. This is combined with the clusterName for the "user" portion of the kubeconfig. Defaults to `nickname`. | 28 | | `emailClaim` | Deprecated. Defaults to `email`. | 29 | | `apiServerURL` | The API server endpoint used to configure kubectl | 30 | | `clusterCAPath` | The path to find the CA bundle for the API server. Used to configure kubectl. This is typically mounted into the default location for workloads running on a Kubernetes cluster and doesn't need to be set. Defaults to `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` | 31 | | `trustedCAPath` | The path to a root CA to trust for self signed certificates at the Oauth2 URLs | 32 | | `httpPath` | The path gangway uses to create urls. Defaults to `""`. | 33 | | `customHTMLTemplatesDir` | The path to a directory that contains custom HTML templates. | 34 | -------------------------------------------------------------------------------- /docs/custom-templates.md: -------------------------------------------------------------------------------- 1 | # Custom Templates 2 | 3 | To customize the HTML pages rendered by Gangway, you may provide a set of custom templates to use instead of the built-in ones. 4 | 5 | :exclamation: **Important: The data passed to the templates might change between versions, and we do not guarantee that we will maintain backwards compatibility. If using custom templates, extra care must be taken when upgrading Gangway.** 6 | 7 | To enable this feature, set the `customHTMLTemplatesDir` option in Gangway's configuration file to a directory that contains the following custom templates: 8 | 9 | * home.tmpl: Home page template. 10 | * commandline.tmpl: Post-login template that typically lists the commands needed to configure `kubectl`. 11 | 12 | The templates are processed using Go's `html/template` [package][0]. 13 | 14 | [0]: https://golang.org/pkg/html/template/ 15 | -------------------------------------------------------------------------------- /docs/dex.md: -------------------------------------------------------------------------------- 1 | # Connecting Gangway to Dex 2 | 3 | [Dex](https://github.com/coreos/dex) is a handy tool created by CoreOS that provides a common OIDC endpoint for multiple identity providers. 4 | To configure Gangway to communicate with Dex some information will need to be collected from Dex. 5 | Following OIDC standard, Dex provides a URL where its OIDC configuration can be gathered. 6 | This URL is located at `.well-known/openid-configuration`. 7 | If Dex is configured with an Issuer URL of `http://app.example.com` its OpenID config can be found at `http://app.example.com/.well-known/openid-configuration`. 8 | An example of the OpenID Configuration provided by Dex: 9 | 10 | ``` 11 | { 12 | "issuer": "http://app.example.com", 13 | "authorization_endpoint": "http://app.example.com/auth", 14 | "token_endpoint": "http://app.example.com/token", 15 | "jwks_uri": "http:/app.example.com/keys", 16 | "response_types_supported": [ 17 | "code" 18 | ], 19 | "subject_types_supported": [ 20 | "public" 21 | ], 22 | "id_token_signing_alg_values_supported": [ 23 | "RS256" 24 | ], 25 | "scopes_supported": [ 26 | "openid", 27 | "email", 28 | "groups", 29 | "profile", 30 | "offline_access" 31 | ], 32 | "token_endpoint_auth_methods_supported": [ 33 | "client_secret_basic" 34 | ], 35 | "claims_supported": [ 36 | "aud", 37 | "email", 38 | "email_verified", 39 | "exp", 40 | "iat", 41 | "iss", 42 | "locale", 43 | "name", 44 | "sub" 45 | ] 46 | } 47 | ``` 48 | 49 | Using the Gangway example Dex's `authorization_endpoint` can be used for `authorize_url` and `token_endpoint` can be used for `token_url`. 50 | The Dex configuration provides a list named `claims_supported` which can be chosen from when defining both `username_claim` and `email_claim`. 51 | The correct claim to use depends on the upstream identity provider that dex is configured for. 52 | `client_id` and `client_secret` are strings that can be any value, but they must match the Client ID and Secret in your Dex configuration. 53 | -------------------------------------------------------------------------------- /docs/google.md: -------------------------------------------------------------------------------- 1 | # Connecting gangway to Google 2 | It is possible to use Google as an OAuth provider with gangway. To do so follow the instructions below: 3 | 4 | ## Setting Up Google OAuth 5 | 6 | * Head to Credentials area of Google Cloud: `https://console.cloud.google.com/apis/credentials?project=`. 7 | If previously you haven't created any credentials, you should see an empty list 8 | 9 | ![google oauth empty list](images/goauth-empty.png) 10 | 11 | * In that page, click on "Create credentials". A menu will pop-over. From that menu click on "OAuth client ID". 12 | 13 | ![google oauth menu](images/goauth-add-credentials-menu.png) 14 | 15 | * In the page you will land, choose "Web application" for the type, then give the oath client id a name and fill in the the callback url appropriately, then click "Create". 16 | 17 | ![google oauth settings](images/goauth-client-settings.png) 18 | 19 | * If successful, you'll be prompted in the modal window if you want to copy the client id and secret. Click "OK" to close. 20 | 21 | * In the list, you should see the credentials we just created. To the right, there are 3 action icons. Click on the downward "download" arrow. 22 | 23 | ## Configuring gangway 24 | 25 | You now need to configure gangway. 26 | Here is a typical config file: 27 | 28 | ```yaml 29 | # Your Cluster Name. There's no strict mapping, so it can be anything 30 | clusterName: "your_cluster_name" 31 | 32 | # The URL to send authorize requests to 33 | # leave as is unless Google instructs you otherwise 34 | authorizeUrl: "https://accounts.google.com/o/oauth2/auth" 35 | 36 | # URL to get a token from 37 | # leave as is unless Google instructs you otherwise 38 | # 39 | # kube-apiserver 1.10+ 40 | # the OpenID Connect authenticator no longer accepts tokens from the Google v3 token APIs; users must switch to the "https://www.googleapis.com/oauth2/v4/token" endpoint. 41 | tokenUrl: "https://accounts.google.com/o/oauth2/token" 42 | 43 | # API Client ID. Get from Google credentials "client_id" field 44 | clientId: "12345678901234567890.apps.googleusercontent.com" 45 | 46 | # API Client Secret. Get from Google credentials "client_secret" field 47 | clientSecret: "FRGegerwgfsFE_fefdsf" 48 | 49 | # Endpoint that provides user profile information. 50 | # For Google's purpose is the same as your client_id 51 | audience: "923798723208-9pq62pkrnbhumipnqs4v0a1iu7ij01fo.apps.googleusercontent.com" 52 | 53 | # Where to redirect back to. This should be a URL 54 | # Where gangway is reachable. Cannot be a raw IP address. Must be a valid TLD. 55 | redirectUrl: "https://url.kuberneters.cluster.com/callback" 56 | 57 | # Used to specify the scope of the requested authorisation in OAuth. 58 | # Unlike with Auth0, we do not need "offline" 59 | scopes: ["openid", "profile", "email"] 60 | 61 | # What field to look at in the token to pull the username from, leave as is 62 | usernameClaim: "sub" 63 | 64 | # What field to look at in the token to pull the email from, leave as is 65 | emailClaim: "email" 66 | 67 | # The API server to use when configuring kubectl for the user 68 | apiServerURL: "https://kube-apiserver.yourcluster.com" 69 | ``` -------------------------------------------------------------------------------- /docs/images/gangway-sequence-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/gangway/657c8dfbfc55f3ae47ac01df52aade83fc2b4744/docs/images/gangway-sequence-diagram.png -------------------------------------------------------------------------------- /docs/images/goauth-add-credentials-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/gangway/657c8dfbfc55f3ae47ac01df52aade83fc2b4744/docs/images/goauth-add-credentials-menu.png -------------------------------------------------------------------------------- /docs/images/goauth-client-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/gangway/657c8dfbfc55f3ae47ac01df52aade83fc2b4744/docs/images/goauth-client-settings.png -------------------------------------------------------------------------------- /docs/images/goauth-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/gangway/657c8dfbfc55f3ae47ac01df52aade83fc2b4744/docs/images/goauth-empty.png -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/gangway/657c8dfbfc55f3ae47ac01df52aade83fc2b4744/docs/images/screenshot.png -------------------------------------------------------------------------------- /docs/yaml/01-namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: gangway 5 | -------------------------------------------------------------------------------- /docs/yaml/02-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: gangway 5 | namespace: gangway 6 | data: 7 | gangway.yaml: | 8 | # The address to listen on. Defaults to 0.0.0.0 to listen on all interfaces. 9 | # Env var: GANGWAY_HOST 10 | # host: 0.0.0.0 11 | 12 | # The port to listen on. Defaults to 8080. 13 | # Env var: GANGWAY_PORT 14 | # port: 8080 15 | 16 | # Should Gangway serve TLS vs. plain HTTP? Default: false 17 | # Env var: GANGWAY_SERVE_TLS 18 | # serveTLS: false 19 | 20 | # The public cert file (including root and intermediates) to use when serving 21 | # TLS. 22 | # Env var: GANGWAY_CERT_FILE 23 | # certFile: /etc/gangway/tls/tls.crt 24 | 25 | # The private key file when serving TLS. 26 | # Env var: GANGWAY_KEY_FILE 27 | # keyFile: /etc/gangway/tls/tls.key 28 | 29 | # The cluster name. Used in UI and kubectl config instructions. 30 | # Env var: GANGWAY_CLUSTER_NAME 31 | clusterName: "${GANGWAY_CLUSTER_NAME}" 32 | 33 | # OAuth2 URL to start authorization flow. 34 | # Env var: GANGWAY_AUTHORIZE_URL 35 | authorizeURL: "https://${DNS_NAME}/authorize" 36 | 37 | # OAuth2 URL to obtain access tokens. 38 | # Env var: GANGWAY_TOKEN_URL 39 | tokenURL: "https://${DNS_NAME}/oauth/token" 40 | 41 | # Endpoint that provides user profile information [optional]. Not all providers 42 | # will require this. 43 | # Env var: GANGWAY_AUDIENCE 44 | audience: "https://${DNS_NAME}/userinfo" 45 | 46 | # Used to specify the scope of the requested Oauth authorization. 47 | # scopes: ["openid", "profile", "email", "offline_access"] 48 | 49 | # Where to redirect back to. This should be a URL where gangway is reachable. 50 | # Typically this also needs to be registered as part of the oauth application 51 | # with the oAuth provider. 52 | # Env var: GANGWAY_REDIRECT_URL 53 | redirectURL: "https://${GANGWAY_REDIRECT_URL}/callback" 54 | 55 | # API client ID as indicated by the identity provider 56 | # Env var: GANGWAY_CLIENT_ID 57 | clientID: "${GANGWAY_CLIENT_ID}" 58 | 59 | # API client secret as indicated by the identity provider 60 | # Env var: GANGWAY_CLIENT_SECRET 61 | clientSecret: "${GANGWAY_CLIENT_SECRET}" 62 | 63 | # Some identity providers accept an empty client secret, this 64 | # is not generally considered a good idea. If you have to use an 65 | # empty secret and accept the risks that come with that then you can 66 | # set this to true. 67 | #allowEmptyClientSecret: false 68 | 69 | # The JWT claim to use as the username. This is used in UI. 70 | # Default is "nickname". This is combined with the clusterName 71 | # for the "user" portion of the kubeconfig. 72 | # Env var: GANGWAY_USERNAME_CLAIM 73 | usernameClaim: "sub" 74 | 75 | # The API server endpoint used to configure kubectl 76 | # Env var: GANGWAY_APISERVER_URL 77 | apiServerURL: "https://${GANGWAY_APISERVER_URL}" 78 | 79 | # The path to find the CA bundle for the API server. Used to configure kubectl. 80 | # This is typically mounted into the default location for workloads running on 81 | # a Kubernetes cluster and doesn't need to be set. 82 | # Env var: GANGWAY_CLUSTER_CA_PATH 83 | # clusterCAPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" 84 | 85 | # The path to a root CA to trust for self signed certificates at the Oauth2 URLs 86 | # Env var: GANGWAY_TRUSTED_CA_PATH 87 | #trustedCAPath: /cacerts/rootca.crt 88 | 89 | # The path gangway uses to create urls (defaults to "") 90 | # Env var: GANGWAY_HTTP_PATH 91 | #httpPath: "https://${GANGWAY_HTTP_PATH}" 92 | 93 | # The path to find custom HTML templates 94 | # Env var: GANGWAY_CUSTOM_HTTP_TEMPLATES_DIR 95 | #customHTMLTemplatesDir: /custom-templates 96 | -------------------------------------------------------------------------------- /docs/yaml/03-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: gangway 5 | namespace: gangway 6 | labels: 7 | app: gangway 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: gangway 13 | strategy: 14 | template: 15 | metadata: 16 | labels: 17 | app: gangway 18 | revision: "1" 19 | spec: 20 | containers: 21 | - name: gangway 22 | image: gcr.io/heptio-images/gangway:v3.2.0 23 | imagePullPolicy: Always 24 | command: ["gangway", "-config", "/gangway/gangway.yaml"] 25 | env: 26 | - name: GANGWAY_SESSION_SECURITY_KEY 27 | valueFrom: 28 | secretKeyRef: 29 | name: gangway-key 30 | key: sessionkey 31 | ports: 32 | - name: http 33 | containerPort: 8080 34 | protocol: TCP 35 | resources: 36 | requests: 37 | cpu: "100m" 38 | memory: "128Mi" 39 | limits: 40 | cpu: "200m" 41 | memory: "512Mi" 42 | volumeMounts: 43 | - name: gangway 44 | mountPath: /gangway/ 45 | livenessProbe: 46 | httpGet: 47 | path: / 48 | port: 8080 49 | initialDelaySeconds: 20 50 | timeoutSeconds: 1 51 | periodSeconds: 60 52 | failureThreshold: 3 53 | readinessProbe: 54 | httpGet: 55 | path: / 56 | port: 8080 57 | timeoutSeconds: 1 58 | periodSeconds: 10 59 | failureThreshold: 3 60 | securityContext: 61 | runAsNonRoot: true 62 | runAsUser: 65534 63 | runAsGroup: 65534 64 | volumes: 65 | - name: gangway 66 | configMap: 67 | name: gangway 68 | -------------------------------------------------------------------------------- /docs/yaml/04-service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: gangwaysvc 5 | namespace: gangway 6 | labels: 7 | app: gangway 8 | spec: 9 | type: ClusterIP 10 | ports: 11 | - name: "http" 12 | protocol: TCP 13 | port: 80 14 | targetPort: "http" 15 | selector: 16 | app: gangway 17 | -------------------------------------------------------------------------------- /docs/yaml/05-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: gangway 5 | namespace: gangway 6 | annotations: 7 | kubernetes.io/tls-acme: "true" 8 | certmanager.k8s.io/cluster-issuer: "letsencrypt-staging" 9 | spec: 10 | tls: 11 | - secretName: gangway 12 | hosts: 13 | - ${GANGWAY_HOST} 14 | rules: 15 | - host: ${GANGWAY_HOST} 16 | http: 17 | paths: 18 | - backend: 19 | serviceName: gangwaysvc 20 | servicePort: http 21 | -------------------------------------------------------------------------------- /docs/yaml/role/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: steve-admin 5 | subjects: 6 | - kind: User 7 | name: steves@heptio.com 8 | apiGroup: rbac.authorization.k8s.io 9 | roleRef: 10 | kind: ClusterRole #this must be Role or ClusterRole 11 | name: cluster-admin # this must match the name of the Role or ClusterRole you wish to bind to 12 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/heptiolabs/gangway 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.1.0+incompatible 7 | github.com/ghodss/yaml v1.0.0 8 | github.com/gogo/protobuf v1.1.1 // indirect 9 | github.com/google/gofuzz v1.1.0 // indirect 10 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f // indirect 11 | github.com/gorilla/securecookie v0.0.0-20160422134519-667fe4e3466a 12 | github.com/gorilla/sessions v0.0.0-20160922145804-ca9ada445741 13 | github.com/json-iterator/go v1.1.9 // indirect 14 | github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da 15 | github.com/kelseyhightower/envconfig v1.3.0 16 | github.com/modern-go/reflect2 v1.0.1 // indirect 17 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 18 | github.com/onsi/ginkgo v1.12.0 // indirect 19 | github.com/onsi/gomega v1.9.0 // indirect 20 | github.com/sirupsen/logrus v1.0.3 21 | github.com/spf13/pflag v1.0.5 // indirect 22 | github.com/stretchr/testify v1.5.1 // indirect 23 | golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3 24 | golang.org/x/oauth2 v0.0.0-20171106152852-9ff8ebcc8e24 25 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect 26 | golang.org/x/text v0.3.2 // indirect 27 | google.golang.org/appengine v1.0.0 // indirect 28 | gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect 29 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 30 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect 31 | gopkg.in/inf.v0 v0.9.1 // indirect 32 | gopkg.in/yaml.v2 v2.2.8 33 | k8s.io/apimachinery v0.0.0-20181126191516-4a9a8137c0a1 // indirect 34 | k8s.io/client-go v9.0.0+incompatible 35 | k8s.io/klog v0.1.0 // indirect 36 | sigs.k8s.io/yaml v1.2.0 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/dgrijalva/jwt-go v3.1.0+incompatible h1:FFziAwDQQ2dz1XClWMkwvukur3evtZx7x/wMHKM1i20= 6 | github.com/dgrijalva/jwt-go v3.1.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 7 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 8 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 9 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 10 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 11 | github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= 12 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 13 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 14 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 16 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 17 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 18 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f h1:9oNbS1z4rVpbnkHBdPZU4jo9bSmrLpII768arSyMFgk= 19 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 20 | github.com/gorilla/securecookie v0.0.0-20160422134519-667fe4e3466a h1:YH0IojQwndMQdeRWdw1aPT8bkbiWaYR3WD+Zf5e09DU= 21 | github.com/gorilla/securecookie v0.0.0-20160422134519-667fe4e3466a/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 22 | github.com/gorilla/sessions v0.0.0-20160922145804-ca9ada445741 h1:OuuPl66BpF1q3OEkaPpp+VfzxrBBY62ATGdWqql/XX8= 23 | github.com/gorilla/sessions v0.0.0-20160922145804-ca9ada445741/go.mod h1:+WVp8kdw6VhyKExm03PAMRn2ZxnPtm58pV0dBVPdhHE= 24 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 25 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 26 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 27 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 28 | github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da h1:5y58+OCjoHCYB8182mpf/dEsq0vwTKPOo4zGfH0xW9A= 29 | github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da/go.mod h1:oLH0CmIaxCGXD67VKGR5AacGXZSMznlmeqM8RzPrcY8= 30 | github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM= 31 | github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 32 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 33 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 34 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 35 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 36 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 37 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 38 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 39 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 40 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 41 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 42 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 43 | github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= 44 | github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= 45 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 46 | github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= 47 | github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/sirupsen/logrus v1.0.3 h1:B5C/igNWoiULof20pKfY4VntcIPqKuwEmoLZrabbUrc= 51 | github.com/sirupsen/logrus v1.0.3/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 52 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 53 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 56 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 57 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 58 | golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3 h1:f4/ZD59VsBOaJmWeI2yqtHvJhmRRPzi73C88ZtfhAIk= 59 | golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 60 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= 61 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 62 | golang.org/x/oauth2 v0.0.0-20171106152852-9ff8ebcc8e24 h1:nP0LlV1P7+z/qtbjHygz+Bba7QsbB4MqdhGJmAyicuI= 63 | golang.org/x/oauth2 v0.0.0-20171106152852-9ff8ebcc8e24/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 64 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 66 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 67 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 68 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= 69 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 71 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 72 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 73 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 74 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= 75 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 76 | google.golang.org/appengine v1.0.0 h1:dN4LljjBKVChsv0XCSI+zbyzdqrkEwX5LQFUMRSGqOc= 77 | google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 78 | gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= 79 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 82 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 84 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 85 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= 86 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 87 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 88 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 89 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 90 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 91 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 92 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 93 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 94 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 95 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 96 | k8s.io/apimachinery v0.0.0-20181126191516-4a9a8137c0a1 h1:u/v3rSGNjiTxclqUNHYgSrCIotyczPebwV1FPXtdKRQ= 97 | k8s.io/apimachinery v0.0.0-20181126191516-4a9a8137c0a1/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= 98 | k8s.io/client-go v9.0.0+incompatible h1:2kqW3X2xQ9SbFvWZjGEHBLlWc1LG9JIJNXWkuqwdZ3A= 99 | k8s.io/client-go v9.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 100 | k8s.io/klog v0.1.0 h1:I5HMfc/DtuVaGR1KPwUrTc476K8NCqNBldC7H4dYEzk= 101 | k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 102 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 103 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 104 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Heptio 2 | // Copyright © 2017 Craig Tracey 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "fmt" 19 | "io/ioutil" 20 | "strings" 21 | 22 | "github.com/kelseyhightower/envconfig" 23 | "gopkg.in/yaml.v2" 24 | ) 25 | 26 | // Config the configuration field for gangway 27 | type Config struct { 28 | Host string `yaml:"host"` 29 | Port int `yaml:"port"` 30 | 31 | ClusterName string `yaml:"clusterName" envconfig:"cluster_name"` 32 | AuthorizeURL string `yaml:"authorizeURL" envconfig:"authorize_url"` 33 | TokenURL string `yaml:"tokenURL" envconfig:"token_url"` 34 | ClientID string `yaml:"clientID" envconfig:"client_id"` 35 | ClientSecret string `yaml:"clientSecret" envconfig:"client_secret"` 36 | AllowEmptyClientSecret bool `yaml:"allowEmptyClientSecret" envconfig:"allow_empty_client_secret"` 37 | Audience string `yaml:"audience" envconfig:"audience"` 38 | RedirectURL string `yaml:"redirectURL" envconfig:"redirect_url"` 39 | Scopes []string `yaml:"scopes" envconfig:"scopes"` 40 | UsernameClaim string `yaml:"usernameClaim" envconfig:"username_claim"` 41 | EmailClaim string `yaml:"emailClaim" envconfig:"email_claim"` 42 | ServeTLS bool `yaml:"serveTLS" envconfig:"serve_tls"` 43 | CertFile string `yaml:"certFile" envconfig:"cert_file"` 44 | KeyFile string `yaml:"keyFile" envconfig:"key_file"` 45 | APIServerURL string `yaml:"apiServerURL" envconfig:"apiserver_url"` 46 | ClusterCAPath string `yaml:"clusterCAPath" envconfig:"cluster_ca_path"` 47 | TrustedCAPath string `yaml:"trustedCAPath" envconfig:"trusted_ca_path"` 48 | HTTPPath string `yaml:"httpPath" envconfig:"http_path"` 49 | 50 | SessionSecurityKey string `yaml:"sessionSecurityKey" envconfig:"SESSION_SECURITY_KEY"` 51 | CustomHTMLTemplatesDir string `yaml:"customHTMLTemplatesDir" envconfig:"custom_http_templates_dir"` 52 | } 53 | 54 | // NewConfig returns a Config struct from serialized config file 55 | func NewConfig(configFile string) (*Config, error) { 56 | 57 | cfg := &Config{ 58 | Host: "0.0.0.0", 59 | Port: 8080, 60 | AllowEmptyClientSecret: false, 61 | Scopes: []string{"openid", "profile", "email", "offline_access"}, 62 | UsernameClaim: "nickname", 63 | EmailClaim: "", 64 | ServeTLS: false, 65 | CertFile: "/etc/gangway/tls/tls.crt", 66 | KeyFile: "/etc/gangway/tls/tls.key", 67 | ClusterCAPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", 68 | HTTPPath: "", 69 | } 70 | 71 | if configFile != "" { 72 | data, err := ioutil.ReadFile(configFile) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | err = yaml.Unmarshal([]byte(data), cfg) 78 | if err != nil { 79 | return nil, err 80 | } 81 | } 82 | 83 | err := envconfig.Process("gangway", cfg) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | err = cfg.Validate() 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | // Check for trailing slash on HTTPPath and remove 94 | cfg.HTTPPath = strings.TrimRight(cfg.HTTPPath, "/") 95 | 96 | return cfg, nil 97 | } 98 | 99 | // Validate verifies all properties of config struct are intialized 100 | func (cfg *Config) Validate() error { 101 | checks := []struct { 102 | bad bool 103 | errMsg string 104 | }{ 105 | {cfg.AuthorizeURL == "", "no authorizeURL specified"}, 106 | {cfg.TokenURL == "", "no tokenURL specified"}, 107 | {cfg.ClientID == "", "no clientID specified"}, 108 | {cfg.ClientSecret == "" && !cfg.AllowEmptyClientSecret, "no clientSecret specified"}, 109 | {cfg.RedirectURL == "", "no redirectURL specified"}, 110 | {cfg.SessionSecurityKey == "", "no SessionSecurityKey specified"}, 111 | {cfg.APIServerURL == "", "no apiServerURL specified"}, 112 | } 113 | 114 | for _, check := range checks { 115 | if check.bad { 116 | return fmt.Errorf("invalid config: %s", check.errMsg) 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | // GetRootPathPrefix returns '/' if no prefix is specified, otherwise returns the configured path 123 | func (cfg *Config) GetRootPathPrefix() string { 124 | if len(cfg.HTTPPath) == 0 { 125 | return "/" 126 | } 127 | 128 | return strings.TrimRight(cfg.HTTPPath, "/") 129 | } 130 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Heptio 2 | // Copyright © 2017 Craig Tracey 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "os" 19 | "testing" 20 | ) 21 | 22 | func TestConfigNotFound(t *testing.T) { 23 | _, err := NewConfig("nonexistentfile") 24 | if err == nil { 25 | t.Errorf("Expected config file parsing to file for non-existent config file") 26 | } 27 | } 28 | 29 | func TestEnvionmentOverrides(t *testing.T) { 30 | os.Setenv("GANGWAY_AUTHORIZE_URL", "https://foo.bar/authorize") 31 | os.Setenv("GANGWAY_APISERVER_URL", "https://k8s-api.foo.baz") 32 | os.Setenv("GANGWAY_CLIENT_ID", "foo") 33 | os.Setenv("GANGWAY_CLIENT_SECRET", "bar") 34 | os.Setenv("GANGWAY_PORT", "1234") 35 | os.Setenv("GANGWAY_REDIRECT_URL", "https://foo.baz/callback") 36 | os.Setenv("GANGWAY_CLUSTER_CA_PATH", "/etc/ssl/certs/ca-certificates.crt") 37 | os.Setenv("GANGWAY_SESSION_SECURITY_KEY", "testing") 38 | os.Setenv("GANGWAY_TOKEN_URL", "https://foo.bar/token") 39 | os.Setenv("GANGWAY_AUDIENCE", "foo") 40 | os.Setenv("GANGWAY_SCOPES", "groups,sub") 41 | cfg, err := NewConfig("") 42 | if err != nil { 43 | t.Errorf("Failed to test config overrides with error: %s", err) 44 | } 45 | 46 | if cfg.Port != 1234 { 47 | t.Errorf("Failed to override config with environment") 48 | } 49 | 50 | if cfg.Audience != "foo" { 51 | t.Errorf("Failed to set audience via environment variable. Expected %s but got %s", "foo", cfg.Audience) 52 | } 53 | 54 | if cfg.Scopes[0] != "groups" || cfg.Scopes[1] != "sub" { 55 | t.Errorf("Failed to set scopes via environment variable. Expected %s but got %s", "[groups, sub]", cfg.Scopes) 56 | } 57 | } 58 | 59 | func TestGetRootPathPrefix(t *testing.T) { 60 | tests := map[string]struct { 61 | path string 62 | want string 63 | }{ 64 | "not specified": { 65 | path: "", 66 | want: "/", 67 | }, 68 | "specified": { 69 | path: "/gangway", 70 | want: "/gangway", 71 | }, 72 | "specified default": { 73 | path: "/", 74 | want: "", 75 | }, 76 | } 77 | 78 | for name, tc := range tests { 79 | t.Run(name, func(t *testing.T) { 80 | cfg := &Config{ 81 | HTTPPath: tc.path, 82 | } 83 | 84 | got := cfg.GetRootPathPrefix() 85 | if got != tc.want { 86 | t.Fatalf("GetRootPathPrefix(): want: %v, got: %v", tc.want, got) 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /internal/config/transport.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Heptio 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package config 15 | 16 | import ( 17 | "crypto/tls" 18 | "crypto/x509" 19 | "io/ioutil" 20 | "log" 21 | "net" 22 | "net/http" 23 | "time" 24 | ) 25 | 26 | // TransportConfig describes a configured httpClient 27 | type TransportConfig struct { 28 | HTTPClient *http.Client 29 | } 30 | 31 | // NewTransportConfig returns a TransportConfig with configured httpClient 32 | func NewTransportConfig(trustedCAPath string) *TransportConfig { 33 | rootCAs, _ := x509.SystemCertPool() 34 | if rootCAs == nil { 35 | rootCAs = x509.NewCertPool() 36 | } 37 | 38 | if trustedCAPath != "" { 39 | // Read in the cert file 40 | certs, err := ioutil.ReadFile(trustedCAPath) 41 | if err != nil { 42 | log.Fatalf("Failed to append %q to RootCAs: %v", trustedCAPath, err) 43 | } 44 | 45 | // Append our cert to the system pool 46 | if ok := rootCAs.AppendCertsFromPEM(certs); !ok { 47 | log.Println("No certs appended, using system certs only") 48 | } 49 | } 50 | 51 | // Transport based on http.DefaultTransport 52 | t := &http.Transport{ 53 | Proxy: http.ProxyFromEnvironment, 54 | DialContext: (&net.Dialer{ 55 | Timeout: 30 * time.Second, 56 | KeepAlive: 30 * time.Second, 57 | DualStack: true, 58 | }).DialContext, 59 | MaxIdleConns: 100, 60 | IdleConnTimeout: 90 * time.Second, 61 | TLSHandshakeTimeout: 10 * time.Second, 62 | ExpectContinueTimeout: 1 * time.Second, 63 | TLSClientConfig: &tls.Config{ 64 | RootCAs: rootCAs, 65 | PreferServerCipherSuites: true, 66 | MinVersion: tls.VersionTLS12, 67 | }, 68 | } 69 | 70 | httpClient := &http.Client{ 71 | Transport: t, 72 | } 73 | 74 | return &TransportConfig{ 75 | HTTPClient: httpClient, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/oidc/token.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Heptio 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package oidc 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | "github.com/dgrijalva/jwt-go" 21 | "golang.org/x/oauth2" 22 | ) 23 | 24 | // OAuth2Token is an interface which is used when exchanging an id_token for an access token 25 | type OAuth2Token interface { 26 | Exchange(ctx context.Context, code string) (*oauth2.Token, error) 27 | } 28 | 29 | // Token is an implementation of OAuth2Token Interface 30 | type Token struct { 31 | OAuth2Cfg *oauth2.Config 32 | } 33 | 34 | // ParseToken returns a jwt token from an idToken, returns error if it cannot parse 35 | func ParseToken(idToken, clientSecret string) (*jwt.Token, error) { 36 | token, _ := jwt.Parse(idToken, func(token *jwt.Token) (interface{}, error) { 37 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 38 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 39 | } 40 | return []byte(clientSecret), nil 41 | }) 42 | 43 | return token, nil 44 | } 45 | 46 | // Exchange takes an oauth2 auth token and exchanges for an id_token 47 | func (t *Token) Exchange(ctx context.Context, code string) (*oauth2.Token, error) { 48 | return t.OAuth2Cfg.Exchange(ctx, code) 49 | } 50 | -------------------------------------------------------------------------------- /internal/oidc/token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Heptio 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package oidc 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/dgrijalva/jwt-go" 20 | ) 21 | 22 | func TestParseToken(t *testing.T) { 23 | tests := map[string]struct { 24 | idToken string 25 | clientSecret string 26 | want *jwt.Token 27 | expectError bool 28 | }{ 29 | "default": { 30 | idToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJHYW5nd2F5VGVzdCIsImlhdCI6MTU0MDA0NjM0NywiZXhwIjoxODg3MjAxNTQ3LCJhdWQiOiJnYW5nd2F5LmhlcHRpby5jb20iLCJzdWIiOiJnYW5nd2F5QGhlcHRpby5jb20iLCJHaXZlbk5hbWUiOiJHYW5nIiwiU3VybmFtZSI6IldheSIsIkVtYWlsIjoiZ2FuZ3dheUBoZXB0aW8uY29tIiwiR3JvdXBzIjoiZGV2LGFkbWluIn0.zNG4Dnxr76J0p4phfsAUYWunioct0krkMiunMynlQsU", 31 | clientSecret: "qwertyuiopasdfghjklzxcvbnm123456", 32 | expectError: false, 33 | want: &jwt.Token{ 34 | Raw: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJHYW5nd2F5VGVzdCIsImlhdCI6MTU0MDA0NjM0NywiZXhwIjoxODg3MjAxNTQ3LCJhdWQiOiJnYW5nd2F5LmhlcHRpby5jb20iLCJzdWIiOiJnYW5nd2F5QGhlcHRpby5jb20iLCJHaXZlbk5hbWUiOiJHYW5nIiwiU3VybmFtZSI6IldheSIsIkVtYWlsIjoiZ2FuZ3dheUBoZXB0aW8uY29tIiwiR3JvdXBzIjoiZGV2LGFkbWluIn0.zNG4Dnxr76J0p4phfsAUYWunioct0krkMiunMynlQsU", 35 | Method: jwt.SigningMethodHS256, 36 | Header: map[string]interface{}{ 37 | "typ": "JWT", 38 | "alg": "HS256", 39 | }, 40 | Claims: jwt.MapClaims{ 41 | "aud": "gangway.heptio.com", 42 | "sub": "gangway@heptio.com", 43 | "GivenName": "Gang", 44 | "Email": "gangway@heptio.com", 45 | "Groups": "dev,admin", 46 | "iat": 1.540046347e+09, 47 | "exp": 1.887201547e+09, 48 | "iss": "GangwayTest", 49 | "Surname": "Way", 50 | }, 51 | Signature: "zNG4Dnxr76J0p4phfsAUYWunioct0krkMiunMynlQsU", 52 | Valid: true, 53 | }, 54 | }, 55 | "rsa": { 56 | idToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE", 57 | clientSecret: "", 58 | expectError: false, 59 | want: &jwt.Token{ 60 | Raw: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE", 61 | Method: jwt.SigningMethodRS256, 62 | Header: map[string]interface{}{ 63 | "alg": "RS256", 64 | "typ": "JWT", 65 | }, 66 | Claims: jwt.MapClaims{ 67 | "sub": "1234567890", 68 | "name": "John Doe", 69 | "admin": true, 70 | }, 71 | Signature: "", 72 | Valid: false, 73 | }, 74 | }, 75 | } 76 | 77 | for name, tc := range tests { 78 | t.Run(name, func(t *testing.T) { 79 | 80 | got, err := ParseToken(tc.idToken, tc.clientSecret) 81 | 82 | // If we expect an error, check that it's thrown 83 | if tc.expectError { 84 | if err == nil { 85 | t.Fatalf("Error was returned but not expected: %v", err) 86 | } 87 | } else { 88 | // We don't expect an error, check the result 89 | if got.Valid != tc.want.Valid { 90 | t.Fatalf("Valid: want: %v, got: %v", tc.want, got) 91 | } 92 | if got.Signature != tc.want.Signature { 93 | t.Fatalf("Signature: want: %v, got: %v", tc.want, got) 94 | } 95 | if got.Raw != tc.want.Raw { 96 | t.Fatalf("Raw: want: %v, got: %v", tc.want, got) 97 | } 98 | if got.Method != tc.want.Method { 99 | t.Fatalf("Method: want: %v, got: %v", tc.want, got) 100 | } 101 | if !eq(got.Header, tc.want.Header) { 102 | t.Fatalf("Header: want: %v, got: %v", tc.want, got) 103 | } 104 | if !eq(got.Claims.(jwt.MapClaims), tc.want.Claims.(jwt.MapClaims)) { 105 | t.Fatalf("Header: want: %v, got: %v", tc.want, got) 106 | } 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func eq(a, b map[string]interface{}) bool { 113 | if len(a) != len(b) { 114 | return false 115 | } 116 | 117 | for k, v := range a { 118 | if w, ok := b[k]; !ok || v != w { 119 | return false 120 | } 121 | } 122 | 123 | return true 124 | } 125 | -------------------------------------------------------------------------------- /internal/session/session.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Heptio 2 | // Copyright © 2017 Craig Tracey 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package session 16 | 17 | import ( 18 | "crypto/sha256" 19 | "golang.org/x/crypto/pbkdf2" 20 | "net/http" 21 | ) 22 | 23 | const salt = "MkmfuPNHnZBBivy0L0aW" 24 | 25 | // Session defines a Gangway session 26 | type Session struct { 27 | Session *CustomCookieStore 28 | } 29 | 30 | // New inits a Session with CookieStore 31 | func New(sessionSecurityKey string) *Session { 32 | return &Session{ 33 | Session: NewCustomCookieStore(generateSessionKeys(sessionSecurityKey)), 34 | } 35 | } 36 | 37 | // generateSessionKeys creates a signed encryption key for the cookie store 38 | func generateSessionKeys(sessionSecurityKey string) ([]byte, []byte) { 39 | // Take the configured security key and generate 96 bytes of data. This is 40 | // used as the signing and encryption keys for the cookie store. For details 41 | // on the PBKDF2 function: https://en.wikipedia.org/wiki/PBKDF2 42 | b := pbkdf2.Key( 43 | []byte(sessionSecurityKey), 44 | []byte(salt), 45 | 4096, 96, sha256.New) 46 | 47 | return b[0:64], b[64:96] 48 | } 49 | 50 | // Cleanup removes the current session from the store 51 | func (s *Session) Cleanup(w http.ResponseWriter, r *http.Request, name string) { 52 | session, err := s.Session.Get(r, name) 53 | if err != nil { 54 | http.Error(w, err.Error(), http.StatusInternalServerError) 55 | return 56 | } 57 | session.Options.MaxAge = -1 58 | session.Save(r, w) 59 | } 60 | -------------------------------------------------------------------------------- /internal/session/session_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Heptio 2 | // Copyright © 2017 Craig Tracey 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package session 16 | 17 | import ( 18 | "net/http" 19 | "net/http/httptest" 20 | "testing" 21 | 22 | "github.com/gorilla/sessions" 23 | log "github.com/sirupsen/logrus" 24 | ) 25 | 26 | func TestGenerateSessionKeys(t *testing.T) { 27 | b1, b2 := generateSessionKeys("testing") 28 | 29 | if len(b1) != 64 || len(b2) != 32 { 30 | t.Errorf("Wrong byte length's returned") 31 | return 32 | } 33 | } 34 | 35 | func TestInitSessionStore(t *testing.T) { 36 | s := New("testing") 37 | if s.Session == nil { 38 | t.Errorf("Session Store is nil. Did not get initialized") 39 | return 40 | } 41 | 42 | } 43 | 44 | func TestCleanupSession(t *testing.T) { 45 | s := New("testing") 46 | session := &sessions.Session{} 47 | // create a test http server 48 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 | session, _ = s.Session.Get(r, "gangway") 50 | s.Cleanup(w, r, "gangway") 51 | 52 | })) 53 | defer ts.Close() 54 | _, err := http.Get(ts.URL) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | if session.Options.MaxAge != -1 { 59 | t.Errorf("Session was not reset. Have max age of %d. Should have -1", session.Options.MaxAge) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/session/store.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Heptio 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package session 15 | 16 | import ( 17 | "fmt" 18 | "github.com/gorilla/securecookie" 19 | "github.com/gorilla/sessions" 20 | "net/http" 21 | ) 22 | 23 | // The CustomCookieStore automatically splits cookies with length greater than maxCookieLength into multiple smaller cookies. 24 | // The motivation is the browsers' 4KB limit on cookies, which for instance causes problems for large id_tokens in azure. 25 | 26 | const ( 27 | // Cookies are limited to 4kb including the length of the cookie name, 28 | // the cookie name can be up to 256 bytes 29 | maxCookieLength = 3840 30 | ) 31 | 32 | type CustomCookieStore struct { 33 | *sessions.CookieStore 34 | } 35 | 36 | // Set secureCookie maxLength to an arbitrary (20x4kb) high value since we are no longer limited 37 | func NewCustomCookieStore(keyPairs ...[]byte) *CustomCookieStore { 38 | cookieStore := sessions.NewCookieStore(keyPairs...) 39 | for _, codec := range cookieStore.Codecs { 40 | cookie := codec.(*securecookie.SecureCookie) 41 | cookie.MaxLength(81920) 42 | } 43 | return &CustomCookieStore{cookieStore} 44 | } 45 | 46 | func (s *CustomCookieStore) Get(r *http.Request, name string) (*sessions.Session, error) { 47 | return sessions.GetRegistry(r).Get(s, name) 48 | } 49 | 50 | // In contrast to default implementation, the session values can be partitioned into 51 | // multiple cookies. 52 | // The original cookie is split/joined in its encoded form 53 | func (s *CustomCookieStore) New(r *http.Request, name string) (*sessions.Session, error) { 54 | session := sessions.NewSession(s, name) 55 | opts := *s.Options 56 | session.Options = &opts 57 | session.IsNew = true 58 | cookie := joinSectionCookies(r, name) 59 | var err error 60 | if len(cookie) > 0 { 61 | err = securecookie.DecodeMulti(name, cookie, &session.Values, s.Codecs...) 62 | if err == nil { 63 | session.IsNew = false 64 | } 65 | } 66 | return session, err 67 | } 68 | 69 | // If the cookie length is > maxCookieLength, its value is split into multiple cookies 70 | // fitting into the maxCookieLength limit. 71 | // The resulting section cookies get their index appended to the name. 72 | func (s *CustomCookieStore) Save(r *http.Request, w http.ResponseWriter, 73 | session *sessions.Session) error { 74 | 75 | cookie, err := securecookie.EncodeMulti(session.Name(), session.Values, 76 | s.Codecs...) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | sectionCookies := splitCookie(cookie) 82 | // With a singular section the name is unchanged 83 | if len(sectionCookies) == 1 { 84 | cookieName := session.Name() 85 | http.SetCookie(w, sessions.NewCookie(cookieName, sectionCookies[0], session.Options)) 86 | return nil 87 | } 88 | 89 | for i, value := range sectionCookies { 90 | cookieName := buildSectionCookieName(session.Name(), i) 91 | http.SetCookie(w, sessions.NewCookie(cookieName, value, session.Options)) 92 | } 93 | return nil 94 | } 95 | 96 | // joinCookies concatenates the values of all matching cookies and returns the original, encoded cookievalue string. 97 | func joinSectionCookies(r *http.Request, name string) string { 98 | 99 | // Exact match without index means only a single cookie exists 100 | if c, err := r.Cookie(name); err == nil { 101 | return c.Value 102 | } 103 | 104 | var joinedValue string 105 | for i := 0; true; i++ { 106 | cookieName := buildSectionCookieName(name, i) 107 | if c, err := r.Cookie(cookieName); err == nil { 108 | joinedValue += c.Value 109 | } else { 110 | break 111 | } 112 | } 113 | return joinedValue 114 | } 115 | 116 | // splitCookie splits the original encoded cookie value into a slice of cookies which 117 | // fit within the 4kb cookie limit indexing the cookies from 0 118 | func splitCookie(cookieValue string) []string { 119 | var sectionCookies []string 120 | valueBytes := []byte(cookieValue) 121 | 122 | for len(valueBytes) > 0 { 123 | length := len(valueBytes) 124 | if length > maxCookieLength { 125 | length = maxCookieLength 126 | } 127 | sectionCookies = append(sectionCookies, string(valueBytes[:length])) 128 | valueBytes = valueBytes[length:] 129 | } 130 | return sectionCookies 131 | } 132 | 133 | func buildSectionCookieName(name string, index int) string { 134 | return fmt.Sprintf("%s_%d", name, index) 135 | } 136 | -------------------------------------------------------------------------------- /internal/session/store_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Heptio 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package session 15 | 16 | import ( 17 | "fmt" 18 | "github.com/gorilla/sessions" 19 | log "github.com/sirupsen/logrus" 20 | "math" 21 | "math/rand" 22 | "net/http" 23 | "net/http/httptest" 24 | "strings" 25 | "testing" 26 | ) 27 | 28 | func TestJoinSectionCookies(t *testing.T) { 29 | var originalValue string 30 | var value string 31 | cookies := buildRandomCookies(2, 3800, "test_%d") 32 | buildRequestWithCookies(cookies, func(cookies []*http.Cookie, r *http.Request) { 33 | for _, c := range cookies { 34 | originalValue += c.Value 35 | } 36 | value = joinSectionCookies(r, "test") 37 | }) 38 | if value != originalValue { 39 | t.Errorf("joinSectionCookies value incorrect: \n value: %s \n originalValue: %s", value, originalValue) 40 | } 41 | } 42 | 43 | func TestJoinSectionCookiesSingle(t *testing.T) { 44 | var originalValue string 45 | var value string 46 | cookies := buildRandomCookies(1, 2000, "test_%d") 47 | buildRequestWithCookies(cookies, func(cookies []*http.Cookie, r *http.Request) { 48 | for _, c := range cookies { 49 | originalValue += c.Value 50 | } 51 | value = joinSectionCookies(r, "test") 52 | }) 53 | if value != originalValue { 54 | t.Errorf("joinSectionCookies value incorrect: \n value: %s \n originalValue: %s", value, originalValue) 55 | } 56 | } 57 | 58 | func TestSplitCookie(t *testing.T) { 59 | cookieLength := 8000 60 | originalValue := randStringBytesRmndr(cookieLength) 61 | sectionCookies := splitCookie(originalValue) 62 | expectedCount := int(math.Ceil((float64(cookieLength) / maxCookieLength))) 63 | if len(sectionCookies) != expectedCount { 64 | t.Errorf("splitCookie count incorrect: \n count: %d \n expectedCount: %d", len(sectionCookies), expectedCount) 65 | } 66 | value := strings.Join(sectionCookies, "") 67 | if value != originalValue { 68 | t.Errorf("splitCookie value incorrect: \n value: %s \n originalValue: %s", value, originalValue) 69 | } 70 | } 71 | 72 | func TestSplitCookieSingle(t *testing.T) { 73 | cookieLength := 2000 74 | originalValue := randStringBytesRmndr(cookieLength) 75 | sectionCookies := splitCookie(originalValue) 76 | expectedCount := int(math.Ceil((float64(cookieLength) / maxCookieLength))) 77 | if len(sectionCookies) != expectedCount { 78 | t.Errorf("splitCookie count incorrect: \n count: %d \n expectedCount: %d", len(sectionCookies), expectedCount) 79 | } 80 | } 81 | 82 | func TestSplitCookieSize(t *testing.T) { 83 | cookieLength := 10000 84 | originalValue := randStringBytesRmndr(cookieLength) 85 | sectionCookies := splitCookie(originalValue) 86 | for _, s := range sectionCookies { 87 | if len(s) > maxCookieLength { 88 | t.Errorf("sectionCookie length over limit: \n length: %d", len(s)) 89 | } 90 | } 91 | } 92 | 93 | func TestSplitAndJoin(t *testing.T) { 94 | cookieLength := 10000 95 | originalValue := randStringBytesRmndr(cookieLength) 96 | sectionCookies := splitCookie(originalValue) 97 | cookies := buildCookiesFromValues(sectionCookies, "test_%d") 98 | var value string 99 | buildRequestWithCookies(cookies, func(cookies []*http.Cookie, r *http.Request) { 100 | value = joinSectionCookies(r, "test") 101 | }) 102 | if value != originalValue { 103 | t.Errorf("SplitAndJoin value incorrect: \n value: %s \n originalValue: %s", value, originalValue) 104 | } 105 | } 106 | 107 | // Utility 108 | 109 | type handleReq func([]*http.Cookie, *http.Request) 110 | 111 | func buildRequestWithCookies(cookies []*http.Cookie, fn handleReq) { 112 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 113 | for _, cookie := range cookies { 114 | r.AddCookie(cookie) 115 | } 116 | fn(cookies, r) 117 | })) 118 | defer ts.Close() 119 | _, err := http.Get(ts.URL) 120 | if err != nil { 121 | log.Fatal(err) 122 | } 123 | } 124 | 125 | func buildRandomCookies(cookieCount int, cookieLength int, cookieName string) []*http.Cookie { 126 | sessionOptions := &sessions.Options{} 127 | var cookies []*http.Cookie 128 | for i := 0; i < cookieCount; i++ { 129 | value := randStringBytesRmndr(cookieLength) 130 | cookie := sessions.NewCookie(fmt.Sprintf(cookieName, i), value, sessionOptions) 131 | cookies = append(cookies, cookie) 132 | } 133 | return cookies 134 | } 135 | 136 | func buildCookiesFromValues(values []string, cookieName string) []*http.Cookie { 137 | sessionOptions := &sessions.Options{} 138 | var cookies []*http.Cookie 139 | for i, value := range values { 140 | cookie := sessions.NewCookie(fmt.Sprintf(cookieName, i), value, sessionOptions) 141 | cookies = append(cookies, cookie) 142 | } 143 | return cookies 144 | } 145 | 146 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 147 | 148 | func randStringBytesRmndr(n int) string { 149 | b := make([]byte, n) 150 | for i := range b { 151 | b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] 152 | } 153 | return string(b) 154 | } 155 | -------------------------------------------------------------------------------- /templates/commandline.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Gangway 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 40 |
41 |

42 | Welcome {{ .Username }}. 43 |

44 |
45 | Claims received from the upstream issuer: 46 |
47 |

 48 | {{- range $key, $value := .Claims -}}
 49 |     {{- if eq $key "groups" -}}
 50 |         {{- printf "%s:\n" $key -}}
 51 |         {{- range $groupName := $value -}}
 52 |             {{- printf "- \"%s\"\n" $groupName -}}
 53 |         {{- end -}}
 54 |     {{- else -}}
 55 |         {{- if eq (printf "%T" $value) "string" -}}
 56 |             {{- printf "%s: \"%s\"\n" $key $value -}}
 57 |         {{- else if eq (printf "%T" $value) "bool" -}}
 58 |             {{- if eq $value true -}}
 59 |                 {{- printf "%s: true\n" $key -}}
 60 |             {{- else -}}
 61 |                 {{- printf "%s: false\n" $key -}}
 62 |             {{ end }}
 63 |         {{- else if or (eq (printf "%T" $value) "float64") (eq (printf "%T" $value) "float32") -}}
 64 |             {{- printf "%s: %f\n" $key $value -}}
 65 |         {{- else -}}
 66 |             {{- printf "%s: %d\n" $key $value -}}
 67 |         {{- end -}}
 68 |     {{- end -}}
 69 | {{- end -}}
 70 |             
71 |
72 | In order to get command-line access to the {{ .ClusterName }} Kubernetes cluster, you will need to configure OpenID Connect (OIDC) authentication for your client. 73 |
74 |
75 |

76 | The Kubernetes command-line utility, kubectl, may be installed like so: 77 |

78 |
 79 |              
 80 | $ curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/$(uname | awk '{print tolower($0)}')/amd64/kubectl
 81 | $ chmod +x ./kubectl
 82 | $ sudo mv ./kubectl /usr/local/bin/kubectl
 83 |              
 84 |            
85 |
86 | 87 |
Once kubectl is installed, you may execute the following:
88 |
89 |
 90 |                
 91 | echo "{{ .ClusterCA }}" \ > "ca-{{ .ClusterName }}.pem"
 92 | kubectl config set-cluster "{{ .ClusterName }}" --server={{ .APIServerURL }} --certificate-authority="ca-{{ .ClusterName }}.pem" --embed-certs
 93 | kubectl config set-credentials "{{ .KubeCfgUser }}"  \
 94 |     --auth-provider=oidc  \
 95 |     --auth-provider-arg='idp-issuer-url={{ .IssuerURL }}'  \
 96 |     --auth-provider-arg='client-id={{ .ClientID }}'  \
 97 |     --auth-provider-arg='client-secret={{ .ClientSecret }}' \
 98 |     --auth-provider-arg='refresh-token={{ .RefreshToken }}' \
 99 |     --auth-provider-arg='id-token={{ .IDToken }}'
100 | kubectl config set-context "{{ .ClusterName }}" --cluster="{{ .ClusterName }}" --user="{{ .KubeCfgUser }}"
101 | kubectl config use-context "{{ .ClusterName }}"
102 | rm "ca-{{ .ClusterName }}.pem"
103 |               
104 |             
105 |
106 | 107 | 108 | -------------------------------------------------------------------------------- /templates/home.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Heptio Gangway 7 | 8 | 9 | 10 | 11 | 29 | 30 | 31 | 43 |
44 |
45 |

46 |

Heptio Gangway Kubernetes Authentication

47 |
48 |
This utility will help you authenticate with your Kubernetes cluster with an OpenID Connect (OIDC) flow. Sign in to get started.
49 |
50 |
51 | Sign In 52 |
53 |

54 | 55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | --------------------------------------------------------------------------------