├── .envrc ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── go.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── api.go ├── api_internals_test.go ├── api_test.go ├── clients.go ├── clients_test.go ├── contains.go ├── contains_test.go ├── curl.go ├── curl_test.go ├── generate.sh ├── generated_client.go ├── generated_client_test.go ├── generated_group.go ├── generated_group_test.go ├── generated_identityzone.go ├── generated_identityzone_test.go ├── generated_mfaprovider.go ├── generated_mfaprovider_test.go ├── generated_user.go ├── generated_user_test.go ├── generator ├── generator.go ├── model.gotemplate └── spec.gotemplate ├── go.mod ├── go.sum ├── go_uaa_suite_test.go ├── groups.go ├── groups_test.go ├── health.go ├── health_test.go ├── identity_zone_test.go ├── identity_zones.go ├── info.go ├── info_test.go ├── issuer.go ├── issuer_test.go ├── me.go ├── me_test.go ├── mfa_provider.go ├── mfa_provider_test.go ├── page.go ├── passwordcredentials ├── README.md ├── passwordcredentials.go └── passwordcredentials_test.go ├── request_errors.go ├── roundtrip.go ├── roundtrip_test.go ├── sort.go ├── token_key.go ├── token_key_test.go ├── token_keys.go ├── token_keys_test.go ├── uaa_internals_test.go ├── uaa_test.go ├── uaa_transport.go ├── uaa_transport_test.go ├── url.go ├── url_internal_test.go ├── url_test.go ├── users.go ├── users_test.go └── utils_test.go /.envrc: -------------------------------------------------------------------------------- 1 | export GO111MODULE=on 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '25 0 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: oldstable 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | .DS_Store 14 | /cmd 15 | 16 | 17 | .idea/ 18 | # Created by https://www.gitignore.io/api/go,intellij 19 | # 20 | # ### Go ### 21 | # # Binaries for programs and plugins 22 | # *.exe 23 | # *.exe~ 24 | # *.dll 25 | # *.so 26 | # *.dylib 27 | # 28 | # # Test binary, build with `go test -c` 29 | # *.test 30 | # 31 | # # Output of the go coverage tool, specifically when used with LiteIDE 32 | # *.out 33 | # 34 | # ### Intellij ### 35 | # # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 36 | # # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 37 | # 38 | # # User-specific stuff 39 | # .idea/**/workspace.xml 40 | # .idea/**/tasks.xml 41 | # .idea/**/usage.statistics.xml 42 | # .idea/**/dictionaries 43 | # .idea/**/shelf 44 | # 45 | # # Sensitive or high-churn files 46 | # .idea/**/dataSources/ 47 | # .idea/**/dataSources.ids 48 | # .idea/**/dataSources.local.xml 49 | # .idea/**/sqlDataSources.xml 50 | # .idea/**/dynamic.xml 51 | # .idea/**/uiDesigner.xml 52 | # .idea/**/dbnavigator.xml 53 | # 54 | # # Gradle 55 | # .idea/**/gradle.xml 56 | # .idea/**/libraries 57 | # 58 | # # CMake 59 | # cmake-build-*/ 60 | # 61 | # # Mongo Explorer plugin 62 | # .idea/**/mongoSettings.xml 63 | # 64 | # # File-based project format 65 | # *.iws 66 | # 67 | # # IntelliJ 68 | # out/ 69 | # 70 | # # mpeltonen/sbt-idea plugin 71 | # .idea_modules/ 72 | # 73 | # # JIRA plugin 74 | # atlassian-ide-plugin.xml 75 | # 76 | # # Cursive Clojure plugin 77 | # .idea/replstate.xml 78 | # 79 | # # Crashlytics plugin (for Android Studio and IntelliJ) 80 | # com_crashlytics_export_strings.xml 81 | # crashlytics.properties 82 | # crashlytics-build.properties 83 | # fabric.properties 84 | # 85 | # # Editor-based Rest Client 86 | # .idea/httpRequests 87 | # 88 | # ### Intellij Patch ### 89 | # # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 90 | # 91 | # # *.iml 92 | # # modules.xml 93 | # # .idea/misc.xml 94 | # # *.ipr 95 | # 96 | # # Sonarlint plugin 97 | # .idea/sonarlint 98 | # 99 | # 100 | # # End of https://www.gitignore.io/api/go,intellij 101 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - "1.12" 5 | - "1.13" 6 | 7 | before_install: 8 | - go get -u golang.org/x/tools/cmd/goimports 9 | 10 | script: 11 | - FILES=`find . -iname '*.go' -type f -not -path "./vendor/*"` 12 | # linting 13 | - env GO111MODULE=on goimports -d $FILES 14 | # testing 15 | - env GO111MODULE=on go test -v -race -covermode=atomic -cover ./... 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `go-uaa` [![Travis-CI](https://travis-ci.org/cloudfoundry-community/go-uaa.svg)](https://travis-ci.org/cloudfoundry-community/go-uaa) [![godoc](https://godoc.org/github.com/cloudfoundry-community/go-uaa?status.svg)](http://godoc.org/github.com/cloudfoundry-community/go-uaa) [![Report card](https://goreportcard.com/badge/github.com/cloudfoundry-community/go-uaa)](https://goreportcard.com/report/github.com/cloudfoundry-community/go-uaa) 2 | 3 | ### Overview 4 | 5 | `go-uaa` is a client library for the [UAA API](https://docs.cloudfoundry.org/api/uaa/). It is a [`go module`](https://github.com/golang/go/wiki/Modules). 6 | 7 | ### Usage 8 | 9 | #### Step 1: Add `go-uaa` As A Dependency 10 | ``` 11 | $ go mod init # optional 12 | $ go get -u github.com/cloudfoundry-community/go-uaa 13 | $ cat go.mod 14 | ``` 15 | 16 | ``` 17 | module github.com/cloudfoundry-community/go-uaa/cmd/test 18 | 19 | go 1.13 20 | 21 | require github.com/cloudfoundry-community/go-uaa latest 22 | ``` 23 | 24 | #### Step 2: Construct and Use `uaa.API` 25 | 26 | Construct a `uaa.API` by using `uaa.New(target string, authOpt AuthenticationOption, opts ...Option)`: 27 | * The target is the URL of your UAA API (for example, https://uaa.run.pivotal.io); *do not* include `/oauth/token` suffix 28 | * You must choose one authentication method and supply it as the third argument. There are a number of authentication methods available: 29 | * [`uaa.WithClientCredentials(clientID string, clientSecret string, tokenFormat TokenFormat)`](https://godoc.org/github.com/cloudfoundry-community/go-uaa#WithClientCredentials) 30 | * [`uaa.WithPasswordCredentials(clientID string, clientSecret string, username string, password string, tokenFormat TokenFormat)`](https://godoc.org/github.com/cloudfoundry-community/go-uaa#WithPasswordCredentials) 31 | * [`uaa.WithAuthorizationCode(clientID string, clientSecret string, authorizationCode string, tokenFormat TokenFormat, redirectURL *url.URL)`](https://godoc.org/github.com/cloudfoundry-community/go-uaa#WithAuthorizationCode) 32 | * [`uaa.WithRefreshToken(clientID string, clientSecret string, refreshToken string, tokenFormat TokenFormat)`](https://godoc.org/github.com/cloudfoundry-community/go-uaa#WithRefreshToken) 33 | * [`uaa.WithToken(token *oauth2.Token)`](https://godoc.org/github.com/cloudfoundry-community/go-uaa#WithToken) (this is the only authentication methods that **cannot** automatically refresh the token when it expires) 34 | * You can optionally supply one or more options: 35 | * [`uaa.WithZoneID(zoneID string)`](https://godoc.org/github.com/cloudfoundry-community/go-uaa#WithZoneID) if you want to specify your own [zone ID](https://docs.cloudfoundry.org/uaa/uaa-concepts.html#iz) 36 | * [`uaa.WithClient(client *http.Client)`](https://godoc.org/github.com/cloudfoundry-community/go-uaa#WithClient) if you want to specify your own `http.Client` 37 | * [`uaa.WithSkipSSLValidation(skipSSLValidation bool)`](https://godoc.org/github.com/cloudfoundry-community/go-uaa#WithSkipSSLValidation) if you want to ignore SSL validation issues; this is not recommended, and you should instead ensure you trust the certificate authority that issues the certificates used by UAA 38 | * [`uaa.WithUserAgent(userAgent string)`](https://godoc.org/github.com/cloudfoundry-community/go-uaa#WithUserAgent) if you want to supply your own user agent for requests to the UAA API 39 | * [`uaa.WithVerbosity(verbose bool)`](https://godoc.org/github.com/cloudfoundry-community/go-uaa#WithVerbosity) if you want to enable verbose logging 40 | 41 | ```bash 42 | $ cat main.go 43 | ``` 44 | 45 | ```go 46 | package main 47 | 48 | import ( 49 | "log" 50 | 51 | uaa "github.com/cloudfoundry-community/go-uaa" 52 | ) 53 | 54 | func main() { 55 | // construct the API 56 | api, err := uaa.New( 57 | "https://uaa.example.net", 58 | uaa.WithClientCredentials("client-id", "client-secret", uaa.JSONWebToken), 59 | ) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | // use the API to fetch a user 65 | user, err := api.GetUserByUsername("test@example.net", "uaa", "") 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | log.Printf("Hello, %s\n", user.Name.GivenName) 70 | } 71 | ``` 72 | 73 | ### Experimental 74 | 75 | * For the foreseeable future, releases will be in the `v0.x.y` range 76 | * You should expect breaking changes until `v1.x.y` releases occur 77 | * Notifications of breaking changes will be made via release notes associated with each tag 78 | * You should [use `go modules`](https://blog.golang.org/using-go-modules) with this package 79 | 80 | ### Contributing 81 | 82 | Pull requests welcome. 83 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "reflect" 10 | 11 | pc "github.com/cloudfoundry-community/go-uaa/passwordcredentials" 12 | "golang.org/x/oauth2" 13 | cc "golang.org/x/oauth2/clientcredentials" 14 | ) 15 | 16 | //go:generate go run ./generator/generator.go 17 | 18 | // API is a client to the UAA API. 19 | type API struct { 20 | Client *http.Client 21 | baseClient *http.Client 22 | baseTransport http.RoundTripper 23 | TargetURL *url.URL 24 | redirectURL *url.URL 25 | skipSSLValidation bool 26 | verbose bool 27 | zoneID string 28 | userAgent string 29 | token *oauth2.Token 30 | target string 31 | mode mode 32 | clientID string 33 | clientSecret string 34 | username string 35 | password string 36 | authorizationCode string 37 | refreshToken string 38 | tokenFormat TokenFormat 39 | clientCredentialsConfig *cc.Config 40 | passwordCredentialsConfig *pc.Config 41 | oauthConfig *oauth2.Config 42 | } 43 | 44 | // TokenFormat is the format of a token. 45 | type TokenFormat int 46 | 47 | // Valid TokenFormat values. 48 | const ( 49 | OpaqueToken TokenFormat = iota 50 | JSONWebToken 51 | ) 52 | 53 | func (t TokenFormat) String() string { 54 | if t == OpaqueToken { 55 | return "opaque" 56 | } 57 | if t == JSONWebToken { 58 | return "jwt" 59 | } 60 | return "" 61 | } 62 | 63 | type mode int 64 | 65 | const ( 66 | custom mode = iota 67 | token 68 | clientcredentials 69 | passwordcredentials 70 | authorizationcode 71 | refreshtoken 72 | ) 73 | 74 | type Option interface { 75 | Apply(a *API) 76 | } 77 | 78 | type AuthenticationOption interface { 79 | ApplyAuthentication(a *API) 80 | } 81 | 82 | func New(target string, authOpt AuthenticationOption, opts ...Option) (*API, error) { 83 | a := &API{ 84 | target: target, 85 | mode: custom, 86 | } 87 | authOpt.ApplyAuthentication(a) 88 | defaultClient := &http.Client{Transport: http.DefaultTransport} 89 | defaultClientOption := WithClient(defaultClient) 90 | defaultUserAgentOption := WithUserAgent("go-uaa") 91 | opts = append([]Option{defaultClientOption, defaultUserAgentOption}, opts...) 92 | for _, option := range opts { 93 | option.Apply(a) 94 | } 95 | err := a.configure() 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | return a, nil 101 | } 102 | 103 | func (a *API) Token(ctx context.Context) (*oauth2.Token, error) { 104 | if _, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); !ok { 105 | ctx = context.WithValue(ctx, oauth2.HTTPClient, a.baseClient) 106 | } 107 | 108 | switch a.mode { 109 | case token: 110 | if !a.token.Valid() { 111 | return nil, errors.New("you have supplied an empty, invalid, or expired token to go-uaa") 112 | } 113 | return a.token, nil 114 | case clientcredentials: 115 | if a.clientCredentialsConfig == nil { 116 | return nil, errors.New("you have supplied invalid client credentials configuration to go-uaa") 117 | } 118 | return a.clientCredentialsConfig.Token(ctx) 119 | case authorizationcode: 120 | if a.oauthConfig == nil { 121 | return nil, errors.New("you have supplied invalid authorization code configuration to go-uaa") 122 | } 123 | tokenFormatParam := oauth2.SetAuthURLParam("token_format", a.tokenFormat.String()) 124 | responseTypeParam := oauth2.SetAuthURLParam("response_type", "token") 125 | 126 | return a.oauthConfig.Exchange(ctx, a.authorizationCode, tokenFormatParam, responseTypeParam) 127 | case refreshtoken: 128 | if a.oauthConfig == nil { 129 | return nil, errors.New("you have supplied invalid refresh token configuration to go-uaa") 130 | } 131 | 132 | tokenSource := a.oauthConfig.TokenSource(ctx, &oauth2.Token{ 133 | RefreshToken: a.refreshToken, 134 | }) 135 | 136 | token, err := tokenSource.Token() 137 | return token, requestErrorFromOauthError(err) 138 | case passwordcredentials: 139 | token, err := a.passwordCredentialsConfig.TokenSource(ctx).Token() 140 | return token, requestErrorFromOauthError(err) 141 | } 142 | return nil, errors.New("your configuration provides no way for go-uaa to get a token") 143 | } 144 | 145 | func (a *API) baseTransportIsNil() bool { 146 | if a.baseTransport == nil || reflect.ValueOf(a.baseTransport).IsNil() { 147 | return true 148 | } 149 | return false 150 | } 151 | 152 | func (a *API) configure() error { 153 | err := a.configureTarget() 154 | if err != nil { 155 | return err 156 | } 157 | if a.baseClient == nil { 158 | return errors.New("please ensure you pass a non-nil client to uaa.WithClient, or remove the uaa.WithClient option") 159 | } 160 | if a.baseTransportIsNil() { 161 | a.baseTransport = a.baseClient.Transport 162 | } 163 | if a.baseTransportIsNil() { 164 | a.baseTransport = http.DefaultTransport 165 | } 166 | 167 | a.ensureTransport(a.baseClient.Transport) 168 | wrappedTransport := &uaaTransport{ 169 | base: a.baseClient.Transport, 170 | LoggingEnabled: a.verbose, 171 | } 172 | a.baseClient.Transport = wrappedTransport 173 | switch a.mode { 174 | case token: 175 | err = a.configureToken() 176 | case clientcredentials: 177 | a.configureClientCredentials() 178 | case passwordcredentials: 179 | a.configurePasswordCredentials() 180 | case authorizationcode: 181 | err = a.configureAuthorizationCode() 182 | case refreshtoken: 183 | err = a.configureRefreshToken() 184 | case custom: 185 | if a.Client == nil { 186 | a.Client = a.baseClient 187 | } 188 | default: 189 | return errors.New("please ensure you pass an AuthenticationOption (e.g. WithClientCredentials, WithPasswordCredentials, WithAuthorizationCode, WithRefreshToken, WithToken) to New(), or manually construct a uaa.API and set uaa.API.Client") 190 | } 191 | if err != nil { 192 | return err 193 | } 194 | if a.Client == nil { 195 | return errors.New("Client is nil; please ensure you pass an AuthenticationOption (e.g. WithClientCredentials, WithPasswordCredentials, WithAuthorizationCode, WithRefreshToken, WithToken) to New(), or manually set Client") 196 | } 197 | a.ensureTransport(a.Client.Transport) 198 | return nil 199 | } 200 | 201 | func (a *API) configureTarget() error { 202 | if a.TargetURL != nil { 203 | return nil 204 | } 205 | if a.target == "" && a.TargetURL == nil { 206 | return errors.New("the target is missing") 207 | } 208 | u, err := BuildTargetURL(a.target) 209 | if err != nil { 210 | return err 211 | } 212 | a.TargetURL = u 213 | return nil 214 | } 215 | 216 | type withClient struct { 217 | client *http.Client 218 | } 219 | 220 | func WithClient(client *http.Client) Option { 221 | return &withClient{client: client} 222 | } 223 | 224 | func (w *withClient) Apply(a *API) { 225 | a.baseClient = w.client 226 | } 227 | 228 | type withTransport struct { 229 | transport http.RoundTripper 230 | } 231 | 232 | func WithTransport(transport http.RoundTripper) Option { 233 | return &withTransport{transport: transport} 234 | } 235 | 236 | func (w *withTransport) Apply(a *API) { 237 | a.baseTransport = w.transport 238 | } 239 | 240 | type withSkipSSLValidation struct { 241 | skipSSLValidation bool 242 | } 243 | 244 | func WithSkipSSLValidation(skipSSLValidation bool) Option { 245 | return &withSkipSSLValidation{skipSSLValidation: skipSSLValidation} 246 | } 247 | 248 | func (w *withSkipSSLValidation) Apply(a *API) { 249 | a.skipSSLValidation = w.skipSSLValidation 250 | } 251 | 252 | type withUserAgent struct { 253 | userAgent string 254 | } 255 | 256 | func WithUserAgent(userAgent string) Option { 257 | return &withUserAgent{userAgent: userAgent} 258 | } 259 | 260 | func (w *withUserAgent) Apply(a *API) { 261 | a.userAgent = w.userAgent 262 | } 263 | 264 | type withZoneID struct { 265 | zoneID string 266 | } 267 | 268 | func WithZoneID(zoneID string) Option { 269 | return &withZoneID{zoneID: zoneID} 270 | } 271 | 272 | func (w *withZoneID) Apply(a *API) { 273 | a.zoneID = w.zoneID 274 | } 275 | 276 | type withVerbosity struct { 277 | verbose bool 278 | } 279 | 280 | func WithVerbosity(verbose bool) Option { 281 | return &withVerbosity{verbose: verbose} 282 | } 283 | 284 | func (w *withVerbosity) Apply(a *API) { 285 | a.verbose = w.verbose 286 | } 287 | 288 | type withClientCredentials struct { 289 | clientID string 290 | clientSecret string 291 | tokenFormat TokenFormat 292 | } 293 | 294 | func WithClientCredentials(clientID string, clientSecret string, tokenFormat TokenFormat) AuthenticationOption { 295 | return &withClientCredentials{clientID: clientID, clientSecret: clientSecret, tokenFormat: tokenFormat} 296 | } 297 | 298 | func (w *withClientCredentials) ApplyAuthentication(a *API) { 299 | a.mode = clientcredentials 300 | a.clientID = w.clientID 301 | a.clientSecret = w.clientSecret 302 | a.tokenFormat = w.tokenFormat 303 | } 304 | 305 | func (a *API) configureClientCredentials() { 306 | tokenURL := urlWithPath(*a.TargetURL, "/oauth/token") 307 | v := url.Values{} 308 | v.Add("token_format", a.tokenFormat.String()) 309 | c := &cc.Config{ 310 | ClientID: a.clientID, 311 | ClientSecret: a.clientSecret, 312 | TokenURL: tokenURL.String(), 313 | EndpointParams: v, 314 | AuthStyle: oauth2.AuthStyleInHeader, 315 | } 316 | a.clientCredentialsConfig = c 317 | a.Client = c.Client(context.WithValue( 318 | context.Background(), 319 | oauth2.HTTPClient, 320 | a.baseClient, 321 | )) 322 | } 323 | 324 | type withPasswordCredentials struct { 325 | clientID string 326 | clientSecret string 327 | username string 328 | password string 329 | tokenFormat TokenFormat 330 | } 331 | 332 | func WithPasswordCredentials(clientID string, clientSecret string, username string, password string, tokenFormat TokenFormat) AuthenticationOption { 333 | return &withPasswordCredentials{ 334 | clientID: clientID, 335 | clientSecret: clientSecret, 336 | username: username, 337 | password: password, 338 | tokenFormat: tokenFormat, 339 | } 340 | } 341 | 342 | func (w *withPasswordCredentials) ApplyAuthentication(a *API) { 343 | a.mode = passwordcredentials 344 | a.clientID = w.clientID 345 | a.clientSecret = w.clientSecret 346 | a.username = w.username 347 | a.password = w.password 348 | a.tokenFormat = w.tokenFormat 349 | } 350 | 351 | func (a *API) configurePasswordCredentials() { 352 | tokenURL := urlWithPath(*a.TargetURL, "/oauth/token") 353 | v := url.Values{} 354 | v.Add("token_format", a.tokenFormat.String()) 355 | c := &pc.Config{ 356 | ClientID: a.clientID, 357 | ClientSecret: a.clientSecret, 358 | Username: a.username, 359 | Password: a.password, 360 | Endpoint: oauth2.Endpoint{ 361 | TokenURL: tokenURL.String(), 362 | }, 363 | EndpointParams: v, 364 | } 365 | a.passwordCredentialsConfig = c 366 | a.Client = c.Client(context.WithValue( 367 | context.Background(), 368 | oauth2.HTTPClient, 369 | a.baseClient)) 370 | } 371 | 372 | type withAuthorizationCode struct { 373 | clientID string 374 | clientSecret string 375 | authorizationCode string 376 | redirectURL *url.URL 377 | tokenFormat TokenFormat 378 | } 379 | 380 | func WithAuthorizationCode(clientID string, clientSecret string, authorizationCode string, tokenFormat TokenFormat, redirectURL *url.URL) AuthenticationOption { 381 | return &withAuthorizationCode{ 382 | clientID: clientID, 383 | clientSecret: clientSecret, 384 | authorizationCode: authorizationCode, 385 | tokenFormat: tokenFormat, 386 | redirectURL: redirectURL, 387 | } 388 | } 389 | 390 | func (w *withAuthorizationCode) ApplyAuthentication(a *API) { 391 | a.mode = authorizationcode 392 | a.clientID = w.clientID 393 | a.clientSecret = w.clientSecret 394 | a.authorizationCode = w.authorizationCode 395 | a.tokenFormat = w.tokenFormat 396 | a.redirectURL = w.redirectURL 397 | } 398 | 399 | func (a *API) configureAuthorizationCode() error { 400 | tokenURL := urlWithPath(*a.TargetURL, "/oauth/token") 401 | c := &oauth2.Config{ 402 | ClientID: a.clientID, 403 | ClientSecret: a.clientSecret, 404 | Endpoint: oauth2.Endpoint{ 405 | TokenURL: tokenURL.String(), 406 | AuthStyle: oauth2.AuthStyleInHeader, 407 | }, 408 | RedirectURL: a.redirectURL.String(), 409 | } 410 | a.oauthConfig = c 411 | ctx := context.WithValue(context.Background(), oauth2.HTTPClient, a.baseClient) 412 | 413 | if !a.token.Valid() { 414 | t, err := a.Token(context.Background()) 415 | if err != nil { 416 | return requestErrorFromOauthError(err) 417 | } 418 | a.token = t 419 | } 420 | 421 | a.Client = c.Client(ctx, a.token) 422 | return nil 423 | } 424 | 425 | type withRefreshToken struct { 426 | clientID string 427 | clientSecret string 428 | refreshToken string 429 | tokenFormat TokenFormat 430 | } 431 | 432 | func WithRefreshToken(clientID string, clientSecret string, refreshToken string, tokenFormat TokenFormat) AuthenticationOption { 433 | return &withRefreshToken{ 434 | clientID: clientID, 435 | clientSecret: clientSecret, 436 | refreshToken: refreshToken, 437 | tokenFormat: tokenFormat, 438 | } 439 | } 440 | 441 | func (w *withRefreshToken) ApplyAuthentication(a *API) { 442 | a.mode = refreshtoken 443 | a.clientID = w.clientID 444 | a.clientSecret = w.clientSecret 445 | a.refreshToken = w.refreshToken 446 | a.tokenFormat = w.tokenFormat 447 | } 448 | 449 | func (a *API) configureRefreshToken() error { 450 | tokenURL := urlWithPath(*a.TargetURL, "/oauth/token") 451 | query := tokenURL.Query() 452 | query.Set("token_format", a.tokenFormat.String()) 453 | tokenURL.RawQuery = query.Encode() 454 | c := &oauth2.Config{ 455 | ClientID: a.clientID, 456 | ClientSecret: a.clientSecret, 457 | Endpoint: oauth2.Endpoint{ 458 | TokenURL: tokenURL.String(), 459 | AuthStyle: oauth2.AuthStyleInHeader, 460 | }, 461 | } 462 | a.oauthConfig = c 463 | ctx := context.WithValue(context.Background(), oauth2.HTTPClient, a.baseClient) 464 | 465 | if !a.token.Valid() { 466 | t, err := a.Token(context.Background()) 467 | if err != nil { 468 | return err 469 | } 470 | a.token = t 471 | } 472 | 473 | a.Client = c.Client(ctx, a.token) 474 | return nil 475 | } 476 | 477 | type withToken struct { 478 | token *oauth2.Token 479 | } 480 | 481 | func WithToken(token *oauth2.Token) AuthenticationOption { 482 | return &withToken{token: token} 483 | } 484 | 485 | func (w *withToken) ApplyAuthentication(a *API) { 486 | a.mode = token 487 | a.token = w.token 488 | } 489 | 490 | func (a *API) configureToken() error { 491 | if !a.token.Valid() { 492 | return errors.New("access token is not valid, or is expired") 493 | } 494 | 495 | tokenClient := &http.Client{ 496 | Transport: &tokenTransport{ 497 | underlyingTransport: a.baseClient.Transport, 498 | token: *a.token, 499 | }, 500 | } 501 | 502 | a.Client = tokenClient 503 | return nil 504 | } 505 | 506 | type tokenTransport struct { 507 | underlyingTransport http.RoundTripper 508 | token oauth2.Token 509 | } 510 | 511 | func (t *tokenTransport) RoundTrip(req *http.Request) (*http.Response, error) { 512 | req.Header.Set("Authorization", fmt.Sprintf("%s %s", t.token.Type(), t.token.AccessToken)) 513 | return t.underlyingTransport.RoundTrip(req) 514 | } 515 | 516 | type withNoAuthentication struct { 517 | } 518 | 519 | func WithNoAuthentication() AuthenticationOption { 520 | return &withNoAuthentication{} 521 | } 522 | 523 | func (w *withNoAuthentication) ApplyAuthentication(a *API) { 524 | a.mode = custom 525 | } 526 | -------------------------------------------------------------------------------- /api_internals_test.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/onsi/gomega" 9 | "github.com/sclevine/spec" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | func testAPI(t *testing.T, when spec.G, it spec.S) { 14 | it.Before(func() { 15 | RegisterTestingT(t) 16 | log.SetFlags(log.Lshortfile) 17 | }) 18 | 19 | when("New", func() { 20 | it("sets the zoneID", func() { 21 | api, err := New("https://example.net", WithToken(&oauth2.Token{ 22 | AccessToken: "blergh", 23 | Expiry: time.Now().Add(60 * time.Second), 24 | }), WithZoneID("zone-1")) 25 | Expect(err).NotTo(HaveOccurred()) 26 | Expect(api).NotTo(BeNil()) 27 | Expect(api.zoneID).To(Equal("zone-1")) 28 | }) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /clients.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // ClientsEndpoint is the path to the clients resource. 13 | const ClientsEndpoint string = "/oauth/clients" 14 | 15 | // paginatedClientList is the response from the API for a single page of clients. 16 | type paginatedClientList struct { 17 | Page 18 | Resources []Client `json:"resources"` 19 | Schemas []string `json:"schemas"` 20 | } 21 | 22 | // Client is a UAA client 23 | // http://docs.cloudfoundry.org/api/uaa/version/4.19.0/index.html#clients. 24 | type Client struct { 25 | ClientID string `json:"client_id,omitempty" generator:"id"` 26 | AuthorizedGrantTypes []string `json:"authorized_grant_types,omitempty"` 27 | RedirectURI []string `json:"redirect_uri,omitempty"` 28 | Scope []string `json:"scope,omitempty"` 29 | ResourceIDs []string `json:"resource_ids,omitempty"` 30 | Authorities []string `json:"authorities,omitempty"` 31 | AutoApproveRaw interface{} `json:"autoapprove,omitempty"` 32 | AccessTokenValidity int64 `json:"access_token_validity,omitempty"` 33 | RefreshTokenValidity int64 `json:"refresh_token_validity,omitempty"` 34 | AllowedProviders []string `json:"allowedproviders,omitempty"` 35 | DisplayName string `json:"name,omitempty"` 36 | TokenSalt string `json:"token_salt,omitempty"` 37 | CreatedWith string `json:"createdwith,omitempty"` 38 | ApprovalsDeleted bool `json:"approvals_deleted,omitempty"` 39 | RequiredUserGroups []string `json:"required_user_groups,omitempty"` 40 | ClientSecret string `json:"client_secret,omitempty"` 41 | LastModified int64 `json:"lastModified,omitempty"` 42 | AllowPublic bool `json:"allowpublic,omitempty"` 43 | } 44 | 45 | // Identifier returns the field used to uniquely identify a Client. 46 | func (c Client) Identifier() string { 47 | return c.ClientID 48 | } 49 | 50 | func (c Client) AutoApprove() []string { 51 | switch t := c.AutoApproveRaw.(type) { 52 | case bool: 53 | return []string{strconv.FormatBool(t)} 54 | case string: 55 | return []string{t} 56 | case []string: 57 | return t 58 | } 59 | return []string{} 60 | } 61 | 62 | // GrantType is a type of oauth2 grant. 63 | type GrantType string 64 | 65 | // Valid GrantType values. 66 | const ( 67 | REFRESHTOKEN = GrantType("refresh_token") 68 | AUTHCODE = GrantType("authorization_code") 69 | IMPLICIT = GrantType("implicit") 70 | PASSWORD = GrantType("password") 71 | CLIENTCREDENTIALS = GrantType("client_credentials") 72 | ) 73 | 74 | func errorMissingValueForGrantType(value string, grantType GrantType) error { 75 | return fmt.Errorf("%v must be specified for %v grant type", value, grantType) 76 | } 77 | 78 | func errorMissingValue(value string) error { 79 | return fmt.Errorf("%v must be specified in the client definition", value) 80 | } 81 | 82 | func requireRedirectURIForGrantType(c *Client, grantType GrantType) error { 83 | if contains(c.AuthorizedGrantTypes, string(grantType)) { 84 | if len(c.RedirectURI) == 0 { 85 | return errorMissingValueForGrantType("redirect_uri", grantType) 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | func requireClientSecretForGrantType(c *Client, grantType GrantType) error { 92 | if contains(c.AuthorizedGrantTypes, string(grantType)) { 93 | if c.ClientSecret == "" { 94 | return errorMissingValueForGrantType("client_secret", grantType) 95 | } 96 | } 97 | return nil 98 | } 99 | 100 | func knownGrantTypesStr() string { 101 | grantTypeStrings := []string{} 102 | knownGrantTypes := []GrantType{AUTHCODE, IMPLICIT, PASSWORD, CLIENTCREDENTIALS} 103 | for _, grant := range knownGrantTypes { 104 | grantTypeStrings = append(grantTypeStrings, string(grant)) 105 | } 106 | 107 | return "[" + strings.Join(grantTypeStrings, ", ") + "]" 108 | } 109 | 110 | // Validate returns nil if the client is valid, or an error if it is invalid. 111 | func (c *Client) Validate() error { 112 | if len(c.AuthorizedGrantTypes) == 0 { 113 | return fmt.Errorf("grant type must be one of %v", knownGrantTypesStr()) 114 | } 115 | 116 | if c.ClientID == "" { 117 | return errorMissingValue("client_id") 118 | } 119 | 120 | if err := requireRedirectURIForGrantType(c, AUTHCODE); err != nil { 121 | return err 122 | } 123 | if err := requireClientSecretForGrantType(c, AUTHCODE); err != nil { 124 | return err 125 | } 126 | 127 | if err := requireClientSecretForGrantType(c, CLIENTCREDENTIALS); err != nil { 128 | return err 129 | } 130 | 131 | if err := requireRedirectURIForGrantType(c, IMPLICIT); err != nil { 132 | return err 133 | } 134 | 135 | return nil 136 | } 137 | 138 | type changeSecretBody struct { 139 | ClientID string `json:"clientId,omitempty"` 140 | ClientSecret string `json:"secret,omitempty"` 141 | } 142 | 143 | // ChangeClientSecret updates the secret with the given value for the client 144 | // with the given id 145 | // http://docs.cloudfoundry.org/api/uaa/version/4.14.0/index.html#change-secret. 146 | func (a *API) ChangeClientSecret(id string, newSecret string) error { 147 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s/secret", ClientsEndpoint, id)) 148 | change := &changeSecretBody{ClientID: id, ClientSecret: newSecret} 149 | j, err := json.Marshal(change) 150 | if err != nil { 151 | return err 152 | } 153 | err = a.doJSON(http.MethodPut, &u, bytes.NewBuffer([]byte(j)), nil, true) 154 | if err != nil { 155 | return err 156 | } 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /clients_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | uaa "github.com/cloudfoundry-community/go-uaa" 10 | . "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/ghttp" 12 | "github.com/sclevine/spec" 13 | ) 14 | 15 | const clientResponse string = `{ 16 | "scope" : [ "clients.read", "clients.write" ], 17 | "client_id" : "00000000-0000-0000-0000-000000000001", 18 | "resource_ids" : [ "none" ], 19 | "authorized_grant_types" : [ "client_credentials" ], 20 | "redirect_uri" : [ "http://ant.path.wildcard/**/passback/*", "http://test1.com" ], 21 | "autoapprove" : [ "true" ], 22 | "authorities" : [ "clients.read", "clients.write" ], 23 | "token_salt" : "1SztLL", 24 | "allowedproviders" : [ "uaa", "ldap", "my-saml-provider" ], 25 | "name" : "My Client Name", 26 | "lastModified" : 1502816030525, 27 | "required_user_groups" : [ ] 28 | }` 29 | 30 | const clientListResponse = `{ 31 | "resources" : [ { 32 | "client_id" : "00000000-0000-0000-0000-000000000001" 33 | }, 34 | { 35 | "client_id" : "00000000-0000-0000-0000-000000000002" 36 | }], 37 | "startIndex" : 1, 38 | "itemsPerPage" : 2, 39 | "totalResults" : 6, 40 | "schemas" : [ "http://cloudfoundry.org/schema/scim/oauth-clients-1.0" ] 41 | }` 42 | 43 | var testClientValue uaa.Client = uaa.Client{ 44 | ClientID: "00000000-0000-0000-0000-000000000001", 45 | ClientSecret: "new_secret", 46 | AllowPublic: true, 47 | } 48 | 49 | const testClientJSON string = `{"client_id": "00000000-0000-0000-0000-000000000001", "client_secret": "new_secret", "allowpublic": true}` 50 | 51 | func testClientExtra(t *testing.T, when spec.G, it spec.S) { 52 | it.Before(func() { 53 | RegisterTestingT(t) 54 | }) 55 | 56 | when("GetClient()", func() { 57 | var ( 58 | server *ghttp.Server 59 | a *uaa.API 60 | ) 61 | 62 | it.Before(func() { 63 | server = ghttp.NewServer() 64 | var err error 65 | a, err = uaa.New(server.URL(), uaa.WithNoAuthentication()) 66 | Expect(err).NotTo(HaveOccurred()) 67 | }) 68 | 69 | it.After(func() { 70 | if server != nil { 71 | server.Close() 72 | } 73 | }) 74 | 75 | when("the client returned from the server contains an autoapprove value that is a boolean", func() { 76 | response := `{ 77 | "scope" : [ "clients.read", "clients.write" ], 78 | "client_id" : "00000000-0000-0000-0000-000000000001", 79 | "resource_ids" : [ "none" ], 80 | "authorized_grant_types" : [ "client_credentials" ], 81 | "redirect_uri" : [ "http://ant.path.wildcard/**/passback/*", "http://test1.com" ], 82 | "autoapprove" : true, 83 | "authorities" : [ "clients.read", "clients.write" ], 84 | "token_salt" : "1SztLL", 85 | "allowedproviders" : [ "uaa", "ldap", "my-saml-provider" ], 86 | "name" : "My Client Name", 87 | "lastModified" : 1502816030525, 88 | "required_user_groups" : [ ] 89 | }` 90 | 91 | it.Before(func() { 92 | server.AppendHandlers(ghttp.CombineHandlers( 93 | ghttp.VerifyRequest("GET", uaa.ClientsEndpoint+"/00000000-0000-0000-0000-000000000001"), 94 | ghttp.VerifyHeaderKV("Accept", "application/json"), 95 | ghttp.RespondWith(http.StatusOK, response), 96 | )) 97 | }) 98 | 99 | it("decodes the autoapprove value", func() { 100 | client, err := a.GetClient("00000000-0000-0000-0000-000000000001") 101 | Expect(err).NotTo(HaveOccurred()) 102 | Expect(client.AutoApprove()).To(Equal([]string{"true"})) 103 | }) 104 | }) 105 | 106 | when("the client returned from the server contains an autoapprove value that is a string", func() { 107 | response := `{ 108 | "scope" : [ "clients.read", "clients.write" ], 109 | "client_id" : "00000000-0000-0000-0000-000000000001", 110 | "resource_ids" : [ "none" ], 111 | "authorized_grant_types" : [ "client_credentials" ], 112 | "redirect_uri" : [ "http://ant.path.wildcard/**/passback/*", "http://test1.com" ], 113 | "autoapprove" : "scope", 114 | "authorities" : [ "clients.read", "clients.write" ], 115 | "token_salt" : "1SztLL", 116 | "allowedproviders" : [ "uaa", "ldap", "my-saml-provider" ], 117 | "name" : "My Client Name", 118 | "lastModified" : 1502816030525, 119 | "required_user_groups" : [ ], 120 | "allowpublic" : true 121 | }` 122 | 123 | it.Before(func() { 124 | server.AppendHandlers(ghttp.CombineHandlers( 125 | ghttp.VerifyRequest("GET", uaa.ClientsEndpoint+"/00000000-0000-0000-0000-000000000001"), 126 | ghttp.VerifyHeaderKV("Accept", "application/json"), 127 | ghttp.RespondWith(http.StatusOK, response), 128 | )) 129 | }) 130 | 131 | it("decodes the autoapprove value", func() { 132 | client, err := a.GetClient("00000000-0000-0000-0000-000000000001") 133 | Expect(err).NotTo(HaveOccurred()) 134 | Expect(client.AutoApprove()).To(Equal([]string{"scope"})) 135 | }) 136 | }) 137 | }) 138 | 139 | when("Client.Validate()", func() { 140 | it("rejects empty grant types", func() { 141 | client := uaa.Client{} 142 | err := client.Validate() 143 | Expect(err.Error()).To(Equal(`grant type must be one of [authorization_code, implicit, password, client_credentials]`)) 144 | }) 145 | 146 | when("when authorization_code", func() { 147 | it("requires client_id", func() { 148 | client := uaa.Client{ 149 | AuthorizedGrantTypes: []string{"authorization_code"}, 150 | RedirectURI: []string{"http://localhost:8080"}, 151 | ClientSecret: "secret", 152 | } 153 | 154 | err := client.Validate() 155 | Expect(err).NotTo(BeNil()) 156 | Expect(err.Error()).To(Equal("client_id must be specified in the client definition")) 157 | }) 158 | 159 | it("requires redirect_uri", func() { 160 | client := uaa.Client{ 161 | ClientID: "myclient", 162 | AuthorizedGrantTypes: []string{"authorization_code"}, 163 | ClientSecret: "secret", 164 | } 165 | err := client.Validate() 166 | Expect(err.Error()).To(Equal("redirect_uri must be specified for authorization_code grant type")) 167 | }) 168 | 169 | it("requires client_secret", func() { 170 | client := uaa.Client{ 171 | ClientID: "myclient", 172 | AuthorizedGrantTypes: []string{"authorization_code"}, 173 | RedirectURI: []string{"http://localhost:8080"}, 174 | } 175 | err := client.Validate() 176 | Expect(err.Error()).To(Equal("client_secret must be specified for authorization_code grant type")) 177 | }) 178 | }) 179 | 180 | when("when implicit", func() { 181 | it("requires client_id", func() { 182 | client := uaa.Client{ 183 | AuthorizedGrantTypes: []string{"implicit"}, 184 | RedirectURI: []string{"http://localhost:8080"}, 185 | } 186 | err := client.Validate() 187 | Expect(err).NotTo(BeNil()) 188 | Expect(err.Error()).To(Equal("client_id must be specified in the client definition")) 189 | }) 190 | 191 | it("requires redirect_uri", func() { 192 | client := uaa.Client{ 193 | ClientID: "myclient", 194 | AuthorizedGrantTypes: []string{"implicit"}, 195 | } 196 | err := client.Validate() 197 | Expect(err.Error()).To(Equal("redirect_uri must be specified for implicit grant type")) 198 | }) 199 | 200 | it("does not require client_secret", func() { 201 | client := uaa.Client{ 202 | ClientID: "someclient", 203 | AuthorizedGrantTypes: []string{"implicit"}, 204 | RedirectURI: []string{"http://localhost:8080"}, 205 | } 206 | err := client.Validate() 207 | Expect(err).To(BeNil()) 208 | }) 209 | }) 210 | 211 | when("when client_credentials", func() { 212 | it("requires client_id", func() { 213 | client := uaa.Client{ 214 | AuthorizedGrantTypes: []string{"client_credentials"}, 215 | ClientSecret: "secret", 216 | } 217 | err := client.Validate() 218 | Expect(err).NotTo(BeNil()) 219 | Expect(err.Error()).To(Equal("client_id must be specified in the client definition")) 220 | }) 221 | 222 | it("requires client_secret", func() { 223 | client := uaa.Client{ 224 | ClientID: "myclient", 225 | AuthorizedGrantTypes: []string{"client_credentials"}, 226 | } 227 | err := client.Validate() 228 | Expect(err.Error()).To(Equal("client_secret must be specified for client_credentials grant type")) 229 | }) 230 | }) 231 | 232 | when("when password", func() { 233 | it("requires client_id", func() { 234 | client := uaa.Client{ 235 | AuthorizedGrantTypes: []string{"password"}, 236 | RedirectURI: []string{"http://localhost:8080"}, 237 | ClientSecret: "secret", 238 | } 239 | err := client.Validate() 240 | Expect(err).NotTo(BeNil()) 241 | Expect(err.Error()).To(Equal("client_id must be specified in the client definition")) 242 | }) 243 | }) 244 | }) 245 | 246 | when("ChangeClientSecret()", func() { 247 | var ( 248 | s *httptest.Server 249 | handler http.Handler 250 | called int 251 | a *uaa.API 252 | ) 253 | 254 | it.Before(func() { 255 | RegisterTestingT(t) 256 | called = 0 257 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 258 | called = called + 1 259 | Expect(handler).NotTo(BeNil()) 260 | handler.ServeHTTP(w, req) 261 | })) 262 | a, _ = uaa.New(s.URL, uaa.WithNoAuthentication()) 263 | }) 264 | 265 | it.After(func() { 266 | if s != nil { 267 | s.Close() 268 | } 269 | }) 270 | 271 | it("calls the /oauth/clients//secret endpoint", func() { 272 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 273 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 274 | Expect(req.Header.Get("Content-Type")).To(Equal("application/json")) 275 | Expect(req.Method).To(Equal(http.MethodPut)) 276 | Expect(req.URL.Path).To(Equal(uaa.ClientsEndpoint + "/00000000-0000-0000-0000-000000000001/secret")) 277 | defer req.Body.Close() 278 | body, _ := ioutil.ReadAll(req.Body) 279 | Expect(body).To(MatchJSON(`{"clientId": "00000000-0000-0000-0000-000000000001", "secret": "new_secret"}`)) 280 | w.WriteHeader(http.StatusOK) 281 | }) 282 | err := a.ChangeClientSecret("00000000-0000-0000-0000-000000000001", "new_secret") 283 | Expect(err).NotTo(HaveOccurred()) 284 | Expect(called).To(Equal(1)) 285 | }) 286 | 287 | it("does not panic when error happens during network call", func() { 288 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 289 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 290 | Expect(req.Header.Get("Content-Type")).To(Equal("application/json")) 291 | Expect(req.Method).To(Equal(http.MethodPut)) 292 | Expect(req.URL.Path).To(Equal(uaa.ClientsEndpoint + "/00000000-0000-0000-0000-000000000001/secret")) 293 | defer req.Body.Close() 294 | body, _ := ioutil.ReadAll(req.Body) 295 | Expect(body).To(MatchJSON(`{"clientId": "00000000-0000-0000-0000-000000000001", "secret": "new_secret"}`)) 296 | w.WriteHeader(http.StatusUnauthorized) 297 | }) 298 | err := a.ChangeClientSecret("00000000-0000-0000-0000-000000000001", "new_secret") 299 | Expect(called).To(Equal(1)) 300 | Expect(err).NotTo(BeNil()) 301 | }) 302 | }) 303 | } 304 | -------------------------------------------------------------------------------- /contains.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | func contains(slice []string, toFind string) bool { 4 | for _, a := range slice { 5 | if a == toFind { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /contains_test.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/gomega" 7 | "github.com/sclevine/spec" 8 | ) 9 | 10 | func testContains(t *testing.T, when spec.G, it spec.S) { 11 | it.Before(func() { 12 | RegisterTestingT(t) 13 | }) 14 | 15 | list := []string{"do", "re", "mi"} 16 | 17 | it("returns true if present", func() { 18 | Expect(contains(list, "re")).To(BeTrue()) 19 | }) 20 | 21 | it("returns false if not present", func() { 22 | Expect(contains(list, "fa")).To(BeFalse()) 23 | }) 24 | 25 | it("handles empty list", func() { 26 | Expect(contains([]string{}, "fa")).To(BeFalse()) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /curl.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httputil" 9 | "net/textproto" 10 | "strings" 11 | ) 12 | 13 | // Curl makes a request to the UAA API with the given path, method, data, and 14 | // headers. 15 | func (a *API) Curl(path string, method string, data string, headers []string) (string, string, int, error) { 16 | u := urlWithPath(*a.TargetURL, path) 17 | req, err := http.NewRequest(method, u.String(), strings.NewReader(data)) 18 | if err != nil { 19 | return "", "", -1, err 20 | } 21 | err = mergeHeaders(req.Header, strings.Join(headers, "\n")) 22 | if err != nil { 23 | return "", "", -1, err 24 | } 25 | 26 | a.ensureTransport(a.Client.Transport) 27 | resp, err := a.Client.Do(req) 28 | if err != nil { 29 | if a.verbose { 30 | fmt.Printf("%v\n\n", err) 31 | } 32 | return "", "", -1, err 33 | } 34 | defer resp.Body.Close() 35 | 36 | headerBytes, _ := httputil.DumpResponse(resp, false) 37 | resHeaders := string(headerBytes) 38 | 39 | bytes, err := ioutil.ReadAll(resp.Body) 40 | if err != nil && a.verbose { 41 | fmt.Printf("%v\n\n", err) 42 | } 43 | resBody := string(bytes) 44 | 45 | return resHeaders, resBody, resp.StatusCode, nil 46 | } 47 | 48 | func mergeHeaders(destination http.Header, headerString string) (err error) { 49 | headerString = strings.TrimSpace(headerString) 50 | headerString += "\n\n" 51 | headerReader := bufio.NewReader(strings.NewReader(headerString)) 52 | headers, err := textproto.NewReader(headerReader).ReadMIMEHeader() 53 | if err != nil { 54 | return 55 | } 56 | 57 | for key, values := range headers { 58 | destination.Del(key) 59 | for _, value := range values { 60 | destination.Add(key, value) 61 | } 62 | } 63 | 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /curl_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | uaa "github.com/cloudfoundry-community/go-uaa" 11 | . "github.com/onsi/gomega" 12 | "github.com/sclevine/spec" 13 | ) 14 | 15 | func testCurl(t *testing.T, when spec.G, it spec.S) { 16 | var ( 17 | s *httptest.Server 18 | handler http.Handler 19 | called int 20 | a *uaa.API 21 | ) 22 | 23 | it.Before(func() { 24 | RegisterTestingT(t) 25 | called = 0 26 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 27 | called = called + 1 28 | Expect(handler).NotTo(BeNil()) 29 | handler.ServeHTTP(w, req) 30 | })) 31 | a, _ = uaa.New(s.URL, uaa.WithNoAuthentication()) 32 | }) 33 | 34 | it.After(func() { 35 | if s != nil { 36 | s.Close() 37 | } 38 | }) 39 | 40 | it("gets a user from the UAA by id", func() { 41 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 42 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 43 | Expect(req.URL.Path).To(Equal("/Users/00000000-0000-0000-0000-000000000001")) 44 | Expect(req.Method).To(Equal(http.MethodGet)) 45 | w.WriteHeader(http.StatusOK) 46 | _, err := w.Write([]byte(userResponse)) 47 | Expect(err).NotTo(HaveOccurred()) 48 | }) 49 | 50 | _, resBody, status, err := a.Curl("/Users/00000000-0000-0000-0000-000000000001", "GET", "", []string{"Accept: application/json"}) 51 | Expect(err).NotTo(HaveOccurred()) 52 | 53 | var user uaa.User 54 | err = json.Unmarshal([]byte(resBody), &user) 55 | Expect(err).NotTo(HaveOccurred()) 56 | 57 | Expect(user.ID).To(Equal("00000000-0000-0000-0000-000000000001")) 58 | Expect(status).To(Equal(http.StatusOK)) 59 | }) 60 | 61 | it("can POST body and multiple headers", func() { 62 | reqBody := map[string]interface{}{ 63 | "externalID": "marcus-user", 64 | "userName": "marcus@stoicism.com", 65 | } 66 | reqBodyBytes, err := json.Marshal(reqBody) 67 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 68 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 69 | Expect(req.Header.Get("Content-Type")).To(Equal("application/json")) 70 | Expect(req.Method).To(Equal(http.MethodPost)) 71 | Expect(req.URL.Path).To(Equal("/Users")) 72 | defer req.Body.Close() 73 | body, _ := ioutil.ReadAll(req.Body) 74 | Expect(body).To(MatchJSON(reqBodyBytes)) 75 | w.WriteHeader(http.StatusOK) 76 | _, err := w.Write([]byte(userResponse)) 77 | Expect(err).NotTo(HaveOccurred()) 78 | }) 79 | 80 | Expect(err).NotTo(HaveOccurred()) 81 | 82 | _, resBody, status, _ := a.Curl("/Users", "POST", string(reqBodyBytes), []string{"Content-Type: application/json", "Accept: application/json"}) 83 | 84 | var user uaa.User 85 | err = json.Unmarshal([]byte(resBody), &user) 86 | Expect(err).NotTo(HaveOccurred()) 87 | 88 | Expect(user.ID).To(Equal("00000000-0000-0000-0000-000000000001")) 89 | Expect(status).To(Equal(http.StatusOK)) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | DIR="$(dirname "${BASH_SOURCE[0]}")" 4 | cd "$DIR" 5 | go generate ./... 6 | find . -name '*.go' -type f -not -path './vendor/*' -exec go fmt {} \;; 7 | cd - 8 | -------------------------------------------------------------------------------- /generated_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-uaa/generator; DO NOT EDIT. 2 | 3 | package uaa 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | ) 14 | 15 | // GetClient with the given clientID. 16 | func (a *API) GetClient(clientID string) (*Client, error) { 17 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", ClientsEndpoint, clientID)) 18 | client := &Client{} 19 | err := a.doJSON(http.MethodGet, &u, nil, client, true) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return client, nil 24 | } 25 | 26 | // CreateClient creates the given client. 27 | func (a *API) CreateClient(client Client) (*Client, error) { 28 | u := urlWithPath(*a.TargetURL, ClientsEndpoint) 29 | created := &Client{} 30 | j, err := json.Marshal(client) 31 | if err != nil { 32 | return nil, err 33 | } 34 | err = a.doJSON(http.MethodPost, &u, bytes.NewBuffer([]byte(j)), created, true) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return created, nil 39 | } 40 | 41 | // UpdateClient updates the given client. 42 | func (a *API) UpdateClient(client Client) (*Client, error) { 43 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", ClientsEndpoint, client.Identifier())) 44 | 45 | created := &Client{} 46 | j, err := json.Marshal(client) 47 | if err != nil { 48 | return nil, err 49 | } 50 | err = a.doJSON(http.MethodPut, &u, bytes.NewBuffer([]byte(j)), created, true) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return created, nil 55 | } 56 | 57 | // DeleteClient deletes the client with the given client ID. 58 | func (a *API) DeleteClient(clientID string) (*Client, error) { 59 | if clientID == "" { 60 | return nil, errors.New("clientID cannot be blank") 61 | } 62 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", ClientsEndpoint, clientID)) 63 | deleted := &Client{} 64 | err := a.doJSON(http.MethodDelete, &u, nil, deleted, true) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return deleted, nil 69 | } 70 | 71 | // ListClients with the given filter, sortBy, attributes, sortOrder, startIndex 72 | // (1-based), and count (default 100). 73 | // If successful, ListClients returns the clients and the total itemsPerPage of clients for 74 | // all pages. If unsuccessful, ListClients returns the error. 75 | func (a *API) ListClients(filter string, sortBy string, sortOrder SortOrder, startIndex int, itemsPerPage int) ([]Client, Page, error) { 76 | u := urlWithPath(*a.TargetURL, ClientsEndpoint) 77 | query := url.Values{} 78 | if filter != "" { 79 | query.Set("filter", filter) 80 | } 81 | if sortBy != "" { 82 | query.Set("sortBy", sortBy) 83 | } 84 | if sortOrder != "" { 85 | query.Set("sortOrder", string(sortOrder)) 86 | } 87 | if startIndex == 0 { 88 | startIndex = 1 89 | } 90 | query.Set("startIndex", strconv.Itoa(startIndex)) 91 | if itemsPerPage == 0 { 92 | itemsPerPage = 100 93 | } 94 | query.Set("count", strconv.Itoa(itemsPerPage)) 95 | u.RawQuery = query.Encode() 96 | 97 | clients := &paginatedClientList{} 98 | err := a.doJSON(http.MethodGet, &u, nil, clients, true) 99 | if err != nil { 100 | return nil, Page{}, err 101 | } 102 | page := Page{ 103 | StartIndex: clients.StartIndex, 104 | ItemsPerPage: clients.ItemsPerPage, 105 | TotalResults: clients.TotalResults, 106 | } 107 | return clients.Resources, page, err 108 | } 109 | 110 | // ListAllClients retrieves UAA clients 111 | func (a *API) ListAllClients(filter string, sortBy string, sortOrder SortOrder) ([]Client, error) { 112 | page := Page{ 113 | StartIndex: 1, 114 | ItemsPerPage: 100, 115 | } 116 | var ( 117 | results []Client 118 | currentPage []Client 119 | err error 120 | ) 121 | 122 | for { 123 | currentPage, page, err = a.ListClients(filter, sortBy, sortOrder, page.StartIndex, page.ItemsPerPage) 124 | if err != nil { 125 | return nil, err 126 | } 127 | results = append(results, currentPage...) 128 | 129 | if (page.StartIndex + page.ItemsPerPage) > page.TotalResults { 130 | break 131 | } 132 | page.StartIndex = page.StartIndex + page.ItemsPerPage 133 | } 134 | return results, nil 135 | } 136 | -------------------------------------------------------------------------------- /generated_group.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-uaa/generator; DO NOT EDIT. 2 | 3 | package uaa 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | ) 14 | 15 | // GetGroup with the given groupID. 16 | func (a *API) GetGroup(groupID string) (*Group, error) { 17 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", GroupsEndpoint, groupID)) 18 | group := &Group{} 19 | err := a.doJSON(http.MethodGet, &u, nil, group, true) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return group, nil 24 | } 25 | 26 | // CreateGroup creates the given group. 27 | func (a *API) CreateGroup(group Group) (*Group, error) { 28 | u := urlWithPath(*a.TargetURL, GroupsEndpoint) 29 | created := &Group{} 30 | j, err := json.Marshal(group) 31 | if err != nil { 32 | return nil, err 33 | } 34 | err = a.doJSON(http.MethodPost, &u, bytes.NewBuffer([]byte(j)), created, true) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return created, nil 39 | } 40 | 41 | // UpdateGroup updates the given group. 42 | func (a *API) UpdateGroup(group Group) (*Group, error) { 43 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", GroupsEndpoint, group.Identifier())) 44 | 45 | created := &Group{} 46 | j, err := json.Marshal(group) 47 | if err != nil { 48 | return nil, err 49 | } 50 | err = a.doJSON(http.MethodPut, &u, bytes.NewBuffer([]byte(j)), created, true) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return created, nil 55 | } 56 | 57 | // DeleteGroup deletes the group with the given group ID. 58 | func (a *API) DeleteGroup(groupID string) (*Group, error) { 59 | if groupID == "" { 60 | return nil, errors.New("groupID cannot be blank") 61 | } 62 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", GroupsEndpoint, groupID)) 63 | deleted := &Group{} 64 | err := a.doJSON(http.MethodDelete, &u, nil, deleted, true) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return deleted, nil 69 | } 70 | 71 | // ListGroups with the given filter, sortBy, attributes, sortOrder, startIndex 72 | // (1-based), and count (default 100). 73 | // If successful, ListGroups returns the groups and the total itemsPerPage of groups for 74 | // all pages. If unsuccessful, ListGroups returns the error. 75 | func (a *API) ListGroups(filter string, sortBy string, attributes string, sortOrder SortOrder, startIndex int, itemsPerPage int) ([]Group, Page, error) { 76 | u := urlWithPath(*a.TargetURL, GroupsEndpoint) 77 | query := url.Values{} 78 | if filter != "" { 79 | query.Set("filter", filter) 80 | } 81 | if attributes != "" { 82 | query.Set("attributes", attributes) 83 | } 84 | if sortBy != "" { 85 | query.Set("sortBy", sortBy) 86 | } 87 | if sortOrder != "" { 88 | query.Set("sortOrder", string(sortOrder)) 89 | } 90 | if startIndex == 0 { 91 | startIndex = 1 92 | } 93 | query.Set("startIndex", strconv.Itoa(startIndex)) 94 | if itemsPerPage == 0 { 95 | itemsPerPage = 100 96 | } 97 | query.Set("count", strconv.Itoa(itemsPerPage)) 98 | u.RawQuery = query.Encode() 99 | 100 | groups := &paginatedGroupList{} 101 | err := a.doJSON(http.MethodGet, &u, nil, groups, true) 102 | if err != nil { 103 | return nil, Page{}, err 104 | } 105 | page := Page{ 106 | StartIndex: groups.StartIndex, 107 | ItemsPerPage: groups.ItemsPerPage, 108 | TotalResults: groups.TotalResults, 109 | } 110 | return groups.Resources, page, err 111 | } 112 | 113 | // ListAllGroups retrieves UAA groups 114 | func (a *API) ListAllGroups(filter string, sortBy string, attributes string, sortOrder SortOrder) ([]Group, error) { 115 | page := Page{ 116 | StartIndex: 1, 117 | ItemsPerPage: 100, 118 | } 119 | var ( 120 | results []Group 121 | currentPage []Group 122 | err error 123 | ) 124 | 125 | for { 126 | currentPage, page, err = a.ListGroups(filter, sortBy, attributes, sortOrder, page.StartIndex, page.ItemsPerPage) 127 | if err != nil { 128 | return nil, err 129 | } 130 | results = append(results, currentPage...) 131 | 132 | if (page.StartIndex + page.ItemsPerPage) > page.TotalResults { 133 | break 134 | } 135 | page.StartIndex = page.StartIndex + page.ItemsPerPage 136 | } 137 | return results, nil 138 | } 139 | -------------------------------------------------------------------------------- /generated_identityzone.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-uaa/generator; DO NOT EDIT. 2 | 3 | package uaa 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | ) 12 | 13 | // GetIdentityZone with the given identityzoneID. 14 | func (a *API) GetIdentityZone(identityzoneID string) (*IdentityZone, error) { 15 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", IdentityZonesEndpoint, identityzoneID)) 16 | identityzone := &IdentityZone{} 17 | err := a.doJSON(http.MethodGet, &u, nil, identityzone, true) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return identityzone, nil 22 | } 23 | 24 | // CreateIdentityZone creates the given identityzone. 25 | func (a *API) CreateIdentityZone(identityzone IdentityZone) (*IdentityZone, error) { 26 | u := urlWithPath(*a.TargetURL, IdentityZonesEndpoint) 27 | created := &IdentityZone{} 28 | j, err := json.Marshal(identityzone) 29 | if err != nil { 30 | return nil, err 31 | } 32 | err = a.doJSON(http.MethodPost, &u, bytes.NewBuffer([]byte(j)), created, true) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return created, nil 37 | } 38 | 39 | // UpdateIdentityZone updates the given identityzone. 40 | func (a *API) UpdateIdentityZone(identityzone IdentityZone) (*IdentityZone, error) { 41 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", IdentityZonesEndpoint, identityzone.Identifier())) 42 | 43 | created := &IdentityZone{} 44 | j, err := json.Marshal(identityzone) 45 | if err != nil { 46 | return nil, err 47 | } 48 | err = a.doJSON(http.MethodPut, &u, bytes.NewBuffer([]byte(j)), created, true) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return created, nil 53 | } 54 | 55 | // DeleteIdentityZone deletes the identityzone with the given identityzone ID. 56 | func (a *API) DeleteIdentityZone(identityzoneID string) (*IdentityZone, error) { 57 | if identityzoneID == "" { 58 | return nil, errors.New("identityzoneID cannot be blank") 59 | } 60 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", IdentityZonesEndpoint, identityzoneID)) 61 | deleted := &IdentityZone{} 62 | err := a.doJSON(http.MethodDelete, &u, nil, deleted, true) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return deleted, nil 67 | } 68 | 69 | // ListIdentityZones fetches all of the IdentityZone records. 70 | // If successful, ListIdentityZones returns the identityzones 71 | // If unsuccessful, ListIdentityZones returns the error. 72 | func (a *API) ListIdentityZones() ([]IdentityZone, error) { 73 | u := urlWithPath(*a.TargetURL, IdentityZonesEndpoint) 74 | var identityzones []IdentityZone 75 | err := a.doJSON(http.MethodGet, &u, nil, &identityzones, true) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return identityzones, nil 80 | } 81 | -------------------------------------------------------------------------------- /generated_identityzone_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-uaa/generator; DO NOT EDIT. 2 | 3 | package uaa_test 4 | 5 | import ( 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | uaa "github.com/cloudfoundry-community/go-uaa" 12 | . "github.com/onsi/gomega" 13 | "github.com/sclevine/spec" 14 | ) 15 | 16 | func testIdentityZone(t *testing.T, when spec.G, it spec.S) { 17 | var ( 18 | s *httptest.Server 19 | handler http.Handler 20 | called int 21 | a *uaa.API 22 | ) 23 | 24 | it.Before(func() { 25 | RegisterTestingT(t) 26 | called = 0 27 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 28 | called = called + 1 29 | Expect(handler).NotTo(BeNil()) 30 | handler.ServeHTTP(w, req) 31 | })) 32 | var err error 33 | a, err = uaa.New(s.URL, uaa.WithNoAuthentication()) 34 | Expect(err).NotTo(HaveOccurred()) 35 | }) 36 | 37 | it.After(func() { 38 | if s != nil { 39 | s.Close() 40 | } 41 | }) 42 | 43 | when("GetIdentityZone()", func() { 44 | when("the identityzone is returned from the server", func() { 45 | it.Before(func() { 46 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 47 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 48 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint + "/00000000-0000-0000-0000-000000000001")) 49 | w.WriteHeader(http.StatusOK) 50 | w.Write([]byte(identityzoneResponse)) 51 | }) 52 | }) 53 | it("gets the identityzone from the UAA by ID", func() { 54 | identityzone, err := a.GetIdentityZone("00000000-0000-0000-0000-000000000001") 55 | Expect(err).NotTo(HaveOccurred()) 56 | Expect(identityzone.ID).To(Equal("00000000-0000-0000-0000-000000000001")) 57 | }) 58 | }) 59 | 60 | when("the server errors", func() { 61 | it.Before(func() { 62 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 63 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 64 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint + "/00000000-0000-0000-0000-000000000001")) 65 | w.WriteHeader(http.StatusInternalServerError) 66 | }) 67 | }) 68 | 69 | it("returns helpful error", func() { 70 | identityzone, err := a.GetIdentityZone("00000000-0000-0000-0000-000000000001") 71 | Expect(err).To(HaveOccurred()) 72 | Expect(identityzone).To(BeNil()) 73 | Expect(err.Error()).To(ContainSubstring("An error occurred while calling")) 74 | }) 75 | }) 76 | 77 | when("the server returns unparsable identityzones", func() { 78 | it.Before(func() { 79 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 80 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 81 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint + "/00000000-0000-0000-0000-000000000001")) 82 | w.WriteHeader(http.StatusOK) 83 | w.Write([]byte("{unparsable-json-response}")) 84 | }) 85 | }) 86 | 87 | it("returns helpful error", func() { 88 | identityzone, err := a.GetIdentityZone("00000000-0000-0000-0000-000000000001") 89 | Expect(err).To(HaveOccurred()) 90 | Expect(identityzone).To(BeNil()) 91 | Expect(err.Error()).To(ContainSubstring("An unknown error occurred while parsing response from")) 92 | Expect(err.Error()).To(ContainSubstring("Response was {unparsable-json-response}")) 93 | }) 94 | }) 95 | }) 96 | 97 | when("CreateIdentityZone()", func() { 98 | it("performs a POST with the identityzone data and returns the created identityzone", func() { 99 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 100 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 101 | Expect(req.Header.Get("Content-Type")).To(Equal("application/json")) 102 | Expect(req.Method).To(Equal(http.MethodPost)) 103 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint)) 104 | defer req.Body.Close() 105 | body, _ := ioutil.ReadAll(req.Body) 106 | Expect(body).To(MatchJSON(testIdentityZoneJSON)) 107 | w.WriteHeader(http.StatusCreated) 108 | w.Write([]byte(identityzoneResponse)) 109 | }) 110 | 111 | created, err := a.CreateIdentityZone(testIdentityZoneValue) 112 | Expect(called).To(Equal(1)) 113 | Expect(err).NotTo(HaveOccurred()) 114 | Expect(created).NotTo(BeNil()) 115 | }) 116 | 117 | it("returns error when response cannot be parsed", func() { 118 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 119 | Expect(req.Method).To(Equal(http.MethodPost)) 120 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint)) 121 | w.WriteHeader(http.StatusOK) 122 | w.Write([]byte("{unparseable}")) 123 | }) 124 | created, err := a.CreateIdentityZone(testIdentityZoneValue) 125 | Expect(err).To(HaveOccurred()) 126 | Expect(created).To(BeNil()) 127 | }) 128 | 129 | it("returns error when response is not 200 OK", func() { 130 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 131 | Expect(req.Method).To(Equal(http.MethodPost)) 132 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint)) 133 | w.WriteHeader(http.StatusBadRequest) 134 | }) 135 | created, err := a.CreateIdentityZone(testIdentityZoneValue) 136 | Expect(err).To(HaveOccurred()) 137 | Expect(created).To(BeNil()) 138 | }) 139 | }) 140 | 141 | when("UpdateIdentityZone()", func() { 142 | it("performs a PUT with the identityzone data and returns the updated identityzone", func() { 143 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 144 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 145 | Expect(req.Header.Get("Content-Type")).To(Equal("application/json")) 146 | Expect(req.Method).To(Equal(http.MethodPut)) 147 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint + "/00000000-0000-0000-0000-000000000001")) 148 | defer req.Body.Close() 149 | body, _ := ioutil.ReadAll(req.Body) 150 | Expect(body).To(MatchJSON(testIdentityZoneJSON)) 151 | w.WriteHeader(http.StatusOK) 152 | w.Write([]byte(identityzoneResponse)) 153 | }) 154 | 155 | updated, err := a.UpdateIdentityZone(testIdentityZoneValue) 156 | Expect(called).To(Equal(1)) 157 | Expect(err).NotTo(HaveOccurred()) 158 | Expect(updated).NotTo(BeNil()) 159 | }) 160 | 161 | it("returns error when response cannot be parsed", func() { 162 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 163 | Expect(req.Method).To(Equal(http.MethodPut)) 164 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint + "/00000000-0000-0000-0000-000000000001")) 165 | w.WriteHeader(http.StatusOK) 166 | w.Write([]byte("{unparseable}")) 167 | }) 168 | updated, err := a.UpdateIdentityZone(testIdentityZoneValue) 169 | Expect(err).To(HaveOccurred()) 170 | Expect(updated).To(BeNil()) 171 | }) 172 | 173 | it("returns error when response is not 200 OK", func() { 174 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 175 | Expect(req.Method).To(Equal(http.MethodPut)) 176 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint + "/00000000-0000-0000-0000-000000000001")) 177 | w.WriteHeader(http.StatusBadRequest) 178 | }) 179 | updated, err := a.UpdateIdentityZone(testIdentityZoneValue) 180 | Expect(err).To(HaveOccurred()) 181 | Expect(updated).To(BeNil()) 182 | }) 183 | }) 184 | 185 | when("DeleteIdentityZone()", func() { 186 | it("errors when the identityzoneID is empty", func() { 187 | deleted, err := a.DeleteIdentityZone("") 188 | Expect(called).To(Equal(0)) 189 | Expect(err).To(HaveOccurred()) 190 | Expect(deleted).To(BeNil()) 191 | }) 192 | 193 | it("performs a DELETE for the identityzone", func() { 194 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 195 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 196 | Expect(req.Method).To(Equal(http.MethodDelete)) 197 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint + "/00000000-0000-0000-0000-000000000001")) 198 | w.WriteHeader(http.StatusOK) 199 | w.Write([]byte(identityzoneResponse)) 200 | }) 201 | 202 | deleted, err := a.DeleteIdentityZone("00000000-0000-0000-0000-000000000001") 203 | Expect(called).To(Equal(1)) 204 | Expect(err).NotTo(HaveOccurred()) 205 | Expect(deleted).NotTo(BeNil()) 206 | }) 207 | 208 | it("returns error when response cannot be parsed", func() { 209 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 210 | Expect(req.Method).To(Equal(http.MethodDelete)) 211 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint + "/00000000-0000-0000-0000-000000000001")) 212 | w.WriteHeader(http.StatusOK) 213 | w.Write([]byte("{unparseable}")) 214 | }) 215 | deleted, err := a.DeleteIdentityZone("00000000-0000-0000-0000-000000000001") 216 | Expect(err).To(HaveOccurred()) 217 | Expect(deleted).To(BeNil()) 218 | }) 219 | 220 | it("returns error when response is not 200 OK", func() { 221 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 222 | Expect(req.Method).To(Equal(http.MethodDelete)) 223 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint + "/00000000-0000-0000-0000-000000000001")) 224 | w.WriteHeader(http.StatusBadRequest) 225 | }) 226 | deleted, err := a.DeleteIdentityZone("00000000-0000-0000-0000-000000000001") 227 | Expect(err).To(HaveOccurred()) 228 | Expect(deleted).To(BeNil()) 229 | }) 230 | }) 231 | 232 | when("ListIdentityZones()", func() { 233 | it("can accept a filter query to limit results", func() { 234 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 235 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 236 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint)) 237 | w.WriteHeader(http.StatusOK) 238 | w.Write([]byte(identityzoneListResponse)) 239 | }) 240 | identityzoneList, err := a.ListIdentityZones() 241 | Expect(err).NotTo(HaveOccurred()) 242 | Expect(identityzoneList[0].ID).To(Equal("00000000-0000-0000-0000-000000000001")) 243 | Expect(identityzoneList[1].ID).To(Equal("00000000-0000-0000-0000-000000000002")) 244 | }) 245 | 246 | it("returns an error when the endpoint doesn't respond", func() { 247 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 248 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 249 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint)) 250 | w.WriteHeader(http.StatusInternalServerError) 251 | }) 252 | 253 | identityzoneList, err := a.ListIdentityZones() 254 | Expect(err).To(HaveOccurred()) 255 | Expect(identityzoneList).To(BeNil()) 256 | }) 257 | 258 | it("returns an error when response is unparseable", func() { 259 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 260 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 261 | Expect(req.URL.Path).To(Equal(uaa.IdentityZonesEndpoint)) 262 | w.WriteHeader(http.StatusOK) 263 | w.Write([]byte("{unparsable}")) 264 | }) 265 | identityzoneList, err := a.ListIdentityZones() 266 | Expect(err).To(HaveOccurred()) 267 | Expect(identityzoneList).To(BeNil()) 268 | }) 269 | }) 270 | } 271 | -------------------------------------------------------------------------------- /generated_mfaprovider.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-uaa/generator; DO NOT EDIT. 2 | 3 | package uaa 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | ) 12 | 13 | // GetMFAProvider with the given mfaproviderID. 14 | func (a *API) GetMFAProvider(mfaproviderID string) (*MFAProvider, error) { 15 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", MFAProvidersEndpoint, mfaproviderID)) 16 | mfaprovider := &MFAProvider{} 17 | err := a.doJSON(http.MethodGet, &u, nil, mfaprovider, true) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return mfaprovider, nil 22 | } 23 | 24 | // CreateMFAProvider creates the given mfaprovider. 25 | func (a *API) CreateMFAProvider(mfaprovider MFAProvider) (*MFAProvider, error) { 26 | u := urlWithPath(*a.TargetURL, MFAProvidersEndpoint) 27 | created := &MFAProvider{} 28 | j, err := json.Marshal(mfaprovider) 29 | if err != nil { 30 | return nil, err 31 | } 32 | err = a.doJSON(http.MethodPost, &u, bytes.NewBuffer([]byte(j)), created, true) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return created, nil 37 | } 38 | 39 | // UpdateMFAProvider updates the given mfaprovider. 40 | func (a *API) UpdateMFAProvider(mfaprovider MFAProvider) (*MFAProvider, error) { 41 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", MFAProvidersEndpoint, mfaprovider.Identifier())) 42 | 43 | created := &MFAProvider{} 44 | j, err := json.Marshal(mfaprovider) 45 | if err != nil { 46 | return nil, err 47 | } 48 | err = a.doJSON(http.MethodPut, &u, bytes.NewBuffer([]byte(j)), created, true) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return created, nil 53 | } 54 | 55 | // DeleteMFAProvider deletes the mfaprovider with the given mfaprovider ID. 56 | func (a *API) DeleteMFAProvider(mfaproviderID string) (*MFAProvider, error) { 57 | if mfaproviderID == "" { 58 | return nil, errors.New("mfaproviderID cannot be blank") 59 | } 60 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", MFAProvidersEndpoint, mfaproviderID)) 61 | deleted := &MFAProvider{} 62 | err := a.doJSON(http.MethodDelete, &u, nil, deleted, true) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return deleted, nil 67 | } 68 | 69 | // ListMFAProviders fetches all of the MFAProvider records. 70 | // If successful, ListMFAProviders returns the mfaproviders 71 | // If unsuccessful, ListMFAProviders returns the error. 72 | func (a *API) ListMFAProviders() ([]MFAProvider, error) { 73 | u := urlWithPath(*a.TargetURL, MFAProvidersEndpoint) 74 | var mfaproviders []MFAProvider 75 | err := a.doJSON(http.MethodGet, &u, nil, &mfaproviders, true) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return mfaproviders, nil 80 | } 81 | -------------------------------------------------------------------------------- /generated_mfaprovider_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-uaa/generator; DO NOT EDIT. 2 | 3 | package uaa_test 4 | 5 | import ( 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | uaa "github.com/cloudfoundry-community/go-uaa" 12 | . "github.com/onsi/gomega" 13 | "github.com/sclevine/spec" 14 | ) 15 | 16 | func testMFAProvider(t *testing.T, when spec.G, it spec.S) { 17 | var ( 18 | s *httptest.Server 19 | handler http.Handler 20 | called int 21 | a *uaa.API 22 | ) 23 | 24 | it.Before(func() { 25 | RegisterTestingT(t) 26 | called = 0 27 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 28 | called = called + 1 29 | Expect(handler).NotTo(BeNil()) 30 | handler.ServeHTTP(w, req) 31 | })) 32 | var err error 33 | a, err = uaa.New(s.URL, uaa.WithNoAuthentication()) 34 | Expect(err).NotTo(HaveOccurred()) 35 | }) 36 | 37 | it.After(func() { 38 | if s != nil { 39 | s.Close() 40 | } 41 | }) 42 | 43 | when("GetMFAProvider()", func() { 44 | when("the mfaprovider is returned from the server", func() { 45 | it.Before(func() { 46 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 47 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 48 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint + "/00000000-0000-0000-0000-000000000001")) 49 | w.WriteHeader(http.StatusOK) 50 | w.Write([]byte(mfaproviderResponse)) 51 | }) 52 | }) 53 | it("gets the mfaprovider from the UAA by ID", func() { 54 | mfaprovider, err := a.GetMFAProvider("00000000-0000-0000-0000-000000000001") 55 | Expect(err).NotTo(HaveOccurred()) 56 | Expect(mfaprovider.ID).To(Equal("00000000-0000-0000-0000-000000000001")) 57 | }) 58 | }) 59 | 60 | when("the server errors", func() { 61 | it.Before(func() { 62 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 63 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 64 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint + "/00000000-0000-0000-0000-000000000001")) 65 | w.WriteHeader(http.StatusInternalServerError) 66 | }) 67 | }) 68 | 69 | it("returns helpful error", func() { 70 | mfaprovider, err := a.GetMFAProvider("00000000-0000-0000-0000-000000000001") 71 | Expect(err).To(HaveOccurred()) 72 | Expect(mfaprovider).To(BeNil()) 73 | Expect(err.Error()).To(ContainSubstring("An error occurred while calling")) 74 | }) 75 | }) 76 | 77 | when("the server returns unparsable mfaproviders", func() { 78 | it.Before(func() { 79 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 80 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 81 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint + "/00000000-0000-0000-0000-000000000001")) 82 | w.WriteHeader(http.StatusOK) 83 | w.Write([]byte("{unparsable-json-response}")) 84 | }) 85 | }) 86 | 87 | it("returns helpful error", func() { 88 | mfaprovider, err := a.GetMFAProvider("00000000-0000-0000-0000-000000000001") 89 | Expect(err).To(HaveOccurred()) 90 | Expect(mfaprovider).To(BeNil()) 91 | Expect(err.Error()).To(ContainSubstring("An unknown error occurred while parsing response from")) 92 | Expect(err.Error()).To(ContainSubstring("Response was {unparsable-json-response}")) 93 | }) 94 | }) 95 | }) 96 | 97 | when("CreateMFAProvider()", func() { 98 | it("performs a POST with the mfaprovider data and returns the created mfaprovider", func() { 99 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 100 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 101 | Expect(req.Header.Get("Content-Type")).To(Equal("application/json")) 102 | Expect(req.Method).To(Equal(http.MethodPost)) 103 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint)) 104 | defer req.Body.Close() 105 | body, _ := ioutil.ReadAll(req.Body) 106 | Expect(body).To(MatchJSON(testMFAProviderJSON)) 107 | w.WriteHeader(http.StatusCreated) 108 | w.Write([]byte(mfaproviderResponse)) 109 | }) 110 | 111 | created, err := a.CreateMFAProvider(testMFAProviderValue) 112 | Expect(called).To(Equal(1)) 113 | Expect(err).NotTo(HaveOccurred()) 114 | Expect(created).NotTo(BeNil()) 115 | }) 116 | 117 | it("returns error when response cannot be parsed", func() { 118 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 119 | Expect(req.Method).To(Equal(http.MethodPost)) 120 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint)) 121 | w.WriteHeader(http.StatusOK) 122 | w.Write([]byte("{unparseable}")) 123 | }) 124 | created, err := a.CreateMFAProvider(testMFAProviderValue) 125 | Expect(err).To(HaveOccurred()) 126 | Expect(created).To(BeNil()) 127 | }) 128 | 129 | it("returns error when response is not 200 OK", func() { 130 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 131 | Expect(req.Method).To(Equal(http.MethodPost)) 132 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint)) 133 | w.WriteHeader(http.StatusBadRequest) 134 | }) 135 | created, err := a.CreateMFAProvider(testMFAProviderValue) 136 | Expect(err).To(HaveOccurred()) 137 | Expect(created).To(BeNil()) 138 | }) 139 | }) 140 | 141 | when("UpdateMFAProvider()", func() { 142 | it("performs a PUT with the mfaprovider data and returns the updated mfaprovider", func() { 143 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 144 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 145 | Expect(req.Header.Get("Content-Type")).To(Equal("application/json")) 146 | Expect(req.Method).To(Equal(http.MethodPut)) 147 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint + "/00000000-0000-0000-0000-000000000001")) 148 | defer req.Body.Close() 149 | body, _ := ioutil.ReadAll(req.Body) 150 | Expect(body).To(MatchJSON(testMFAProviderJSON)) 151 | w.WriteHeader(http.StatusOK) 152 | w.Write([]byte(mfaproviderResponse)) 153 | }) 154 | 155 | updated, err := a.UpdateMFAProvider(testMFAProviderValue) 156 | Expect(called).To(Equal(1)) 157 | Expect(err).NotTo(HaveOccurred()) 158 | Expect(updated).NotTo(BeNil()) 159 | }) 160 | 161 | it("returns error when response cannot be parsed", func() { 162 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 163 | Expect(req.Method).To(Equal(http.MethodPut)) 164 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint + "/00000000-0000-0000-0000-000000000001")) 165 | w.WriteHeader(http.StatusOK) 166 | w.Write([]byte("{unparseable}")) 167 | }) 168 | updated, err := a.UpdateMFAProvider(testMFAProviderValue) 169 | Expect(err).To(HaveOccurred()) 170 | Expect(updated).To(BeNil()) 171 | }) 172 | 173 | it("returns error when response is not 200 OK", func() { 174 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 175 | Expect(req.Method).To(Equal(http.MethodPut)) 176 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint + "/00000000-0000-0000-0000-000000000001")) 177 | w.WriteHeader(http.StatusBadRequest) 178 | }) 179 | updated, err := a.UpdateMFAProvider(testMFAProviderValue) 180 | Expect(err).To(HaveOccurred()) 181 | Expect(updated).To(BeNil()) 182 | }) 183 | }) 184 | 185 | when("DeleteMFAProvider()", func() { 186 | it("errors when the mfaproviderID is empty", func() { 187 | deleted, err := a.DeleteMFAProvider("") 188 | Expect(called).To(Equal(0)) 189 | Expect(err).To(HaveOccurred()) 190 | Expect(deleted).To(BeNil()) 191 | }) 192 | 193 | it("performs a DELETE for the mfaprovider", func() { 194 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 195 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 196 | Expect(req.Method).To(Equal(http.MethodDelete)) 197 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint + "/00000000-0000-0000-0000-000000000001")) 198 | w.WriteHeader(http.StatusOK) 199 | w.Write([]byte(mfaproviderResponse)) 200 | }) 201 | 202 | deleted, err := a.DeleteMFAProvider("00000000-0000-0000-0000-000000000001") 203 | Expect(called).To(Equal(1)) 204 | Expect(err).NotTo(HaveOccurred()) 205 | Expect(deleted).NotTo(BeNil()) 206 | }) 207 | 208 | it("returns error when response cannot be parsed", func() { 209 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 210 | Expect(req.Method).To(Equal(http.MethodDelete)) 211 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint + "/00000000-0000-0000-0000-000000000001")) 212 | w.WriteHeader(http.StatusOK) 213 | w.Write([]byte("{unparseable}")) 214 | }) 215 | deleted, err := a.DeleteMFAProvider("00000000-0000-0000-0000-000000000001") 216 | Expect(err).To(HaveOccurred()) 217 | Expect(deleted).To(BeNil()) 218 | }) 219 | 220 | it("returns error when response is not 200 OK", func() { 221 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 222 | Expect(req.Method).To(Equal(http.MethodDelete)) 223 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint + "/00000000-0000-0000-0000-000000000001")) 224 | w.WriteHeader(http.StatusBadRequest) 225 | }) 226 | deleted, err := a.DeleteMFAProvider("00000000-0000-0000-0000-000000000001") 227 | Expect(err).To(HaveOccurred()) 228 | Expect(deleted).To(BeNil()) 229 | }) 230 | }) 231 | 232 | when("ListMFAProviders()", func() { 233 | it("can accept a filter query to limit results", func() { 234 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 235 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 236 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint)) 237 | w.WriteHeader(http.StatusOK) 238 | w.Write([]byte(mfaproviderListResponse)) 239 | }) 240 | mfaproviderList, err := a.ListMFAProviders() 241 | Expect(err).NotTo(HaveOccurred()) 242 | Expect(mfaproviderList[0].ID).To(Equal("00000000-0000-0000-0000-000000000001")) 243 | Expect(mfaproviderList[1].ID).To(Equal("00000000-0000-0000-0000-000000000002")) 244 | }) 245 | 246 | it("returns an error when the endpoint doesn't respond", func() { 247 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 248 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 249 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint)) 250 | w.WriteHeader(http.StatusInternalServerError) 251 | }) 252 | 253 | mfaproviderList, err := a.ListMFAProviders() 254 | Expect(err).To(HaveOccurred()) 255 | Expect(mfaproviderList).To(BeNil()) 256 | }) 257 | 258 | it("returns an error when response is unparseable", func() { 259 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 260 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 261 | Expect(req.URL.Path).To(Equal(uaa.MFAProvidersEndpoint)) 262 | w.WriteHeader(http.StatusOK) 263 | w.Write([]byte("{unparsable}")) 264 | }) 265 | mfaproviderList, err := a.ListMFAProviders() 266 | Expect(err).To(HaveOccurred()) 267 | Expect(mfaproviderList).To(BeNil()) 268 | }) 269 | }) 270 | } 271 | -------------------------------------------------------------------------------- /generated_user.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-uaa/generator; DO NOT EDIT. 2 | 3 | package uaa 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | ) 14 | 15 | // GetUser with the given userID. 16 | func (a *API) GetUser(userID string) (*User, error) { 17 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", UsersEndpoint, userID)) 18 | user := &User{} 19 | err := a.doJSON(http.MethodGet, &u, nil, user, true) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return user, nil 24 | } 25 | 26 | // CreateUser creates the given user. 27 | func (a *API) CreateUser(user User) (*User, error) { 28 | u := urlWithPath(*a.TargetURL, UsersEndpoint) 29 | created := &User{} 30 | j, err := json.Marshal(user) 31 | if err != nil { 32 | return nil, err 33 | } 34 | err = a.doJSON(http.MethodPost, &u, bytes.NewBuffer([]byte(j)), created, true) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return created, nil 39 | } 40 | 41 | // UpdateUser updates the given user. 42 | func (a *API) UpdateUser(user User) (*User, error) { 43 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", UsersEndpoint, user.Identifier())) 44 | 45 | created := &User{} 46 | j, err := json.Marshal(user) 47 | if err != nil { 48 | return nil, err 49 | } 50 | err = a.doJSON(http.MethodPut, &u, bytes.NewBuffer([]byte(j)), created, true) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return created, nil 55 | } 56 | 57 | // DeleteUser deletes the user with the given user ID. 58 | func (a *API) DeleteUser(userID string) (*User, error) { 59 | if userID == "" { 60 | return nil, errors.New("userID cannot be blank") 61 | } 62 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", UsersEndpoint, userID)) 63 | deleted := &User{} 64 | err := a.doJSON(http.MethodDelete, &u, nil, deleted, true) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return deleted, nil 69 | } 70 | 71 | // ListUsers with the given filter, sortBy, attributes, sortOrder, startIndex 72 | // (1-based), and count (default 100). 73 | // If successful, ListUsers returns the users and the total itemsPerPage of users for 74 | // all pages. If unsuccessful, ListUsers returns the error. 75 | func (a *API) ListUsers(filter string, sortBy string, attributes string, sortOrder SortOrder, startIndex int, itemsPerPage int) ([]User, Page, error) { 76 | u := urlWithPath(*a.TargetURL, UsersEndpoint) 77 | query := url.Values{} 78 | if filter != "" { 79 | query.Set("filter", filter) 80 | } 81 | if attributes != "" { 82 | query.Set("attributes", attributes) 83 | } 84 | if sortBy != "" { 85 | query.Set("sortBy", sortBy) 86 | } 87 | if sortOrder != "" { 88 | query.Set("sortOrder", string(sortOrder)) 89 | } 90 | if startIndex == 0 { 91 | startIndex = 1 92 | } 93 | query.Set("startIndex", strconv.Itoa(startIndex)) 94 | if itemsPerPage == 0 { 95 | itemsPerPage = 100 96 | } 97 | query.Set("count", strconv.Itoa(itemsPerPage)) 98 | u.RawQuery = query.Encode() 99 | 100 | users := &paginatedUserList{} 101 | err := a.doJSON(http.MethodGet, &u, nil, users, true) 102 | if err != nil { 103 | return nil, Page{}, err 104 | } 105 | page := Page{ 106 | StartIndex: users.StartIndex, 107 | ItemsPerPage: users.ItemsPerPage, 108 | TotalResults: users.TotalResults, 109 | } 110 | return users.Resources, page, err 111 | } 112 | 113 | // ListAllUsers retrieves UAA users 114 | func (a *API) ListAllUsers(filter string, sortBy string, attributes string, sortOrder SortOrder) ([]User, error) { 115 | page := Page{ 116 | StartIndex: 1, 117 | ItemsPerPage: 100, 118 | } 119 | var ( 120 | results []User 121 | currentPage []User 122 | err error 123 | ) 124 | 125 | for { 126 | currentPage, page, err = a.ListUsers(filter, sortBy, attributes, sortOrder, page.StartIndex, page.ItemsPerPage) 127 | if err != nil { 128 | return nil, err 129 | } 130 | results = append(results, currentPage...) 131 | 132 | if (page.StartIndex + page.ItemsPerPage) > page.TotalResults { 133 | break 134 | } 135 | page.StartIndex = page.StartIndex + page.ItemsPerPage 136 | } 137 | return results, nil 138 | } 139 | -------------------------------------------------------------------------------- /generator/generator.go: -------------------------------------------------------------------------------- 1 | // This program generates files used to access the UAA API. It can be invoked 2 | // by running go generate 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "go/format" 10 | "html/template" 11 | "io" 12 | "io/ioutil" 13 | "log" 14 | "path/filepath" 15 | "reflect" 16 | "runtime" 17 | "strings" 18 | 19 | uaa "github.com/cloudfoundry-community/go-uaa" 20 | ) 21 | 22 | var typesToProcess = []interface{}{ 23 | uaa.Client{}, 24 | uaa.Group{}, 25 | uaa.User{}, 26 | uaa.IdentityZone{}, 27 | uaa.MFAProvider{}, 28 | } 29 | 30 | func main() { 31 | for _, typ := range typesToProcess { 32 | rtype := reflect.TypeOf(typ) 33 | if rtype.Kind() != reflect.Struct { 34 | log.Printf("[warn]: %s is not a struct type...skipping\n", rtype.Name()) 35 | continue 36 | } 37 | 38 | typeName := rtype.Name() 39 | pluralTypeName := typeName + "s" 40 | 41 | t := typeGenerator{ 42 | ModelTypeName: typeName, 43 | ModelPluralTypeName: pluralTypeName, 44 | IDFieldName: "ID", 45 | SupportsAttributes: true, 46 | SupportsPaging: true, 47 | } 48 | if typeName == "Client" { 49 | t.SupportsAttributes = false 50 | } 51 | 52 | if typeName == "IdentityZone" || typeName == "MFAProvider" { 53 | t.SupportsPaging = false 54 | } 55 | 56 | for i := 0; i < rtype.NumField(); i++ { 57 | field := rtype.Field(i) 58 | 59 | fieldTag := field.Tag.Get("generator") 60 | if fieldTag == "id" { 61 | t.IDFieldName = field.Name 62 | } 63 | } 64 | 65 | sourceBuf := &bytes.Buffer{} 66 | specBuf := &bytes.Buffer{} 67 | 68 | generate(sourceBuf, specBuf, t) 69 | 70 | if sourceBuf.Len() > 0 { 71 | writeFile(sourceBuf.Bytes(), strings.ToLower(fmt.Sprintf("generated_%s.go", t.ModelTypeName))) 72 | } 73 | if specBuf.Len() > 0 { 74 | writeFile(specBuf.Bytes(), strings.ToLower(fmt.Sprintf("generated_%s_test.go", t.ModelTypeName))) 75 | } 76 | } 77 | } 78 | 79 | func generate(sourceBuf io.Writer, specBuf io.Writer, t typeGenerator) { 80 | if err := modelTmpl.Execute(sourceBuf, t); err != nil { 81 | log.Fatalf("generating model code: %v", err) 82 | } 83 | 84 | if err := specTmpl.Execute(specBuf, t); err != nil { 85 | log.Fatalf("generating test code: %v", err) 86 | } 87 | } 88 | 89 | func writeFile(buf []byte, filename string) { 90 | src, err := format.Source(buf) 91 | if err != nil { 92 | log.Printf("warning: internal error: invalid Go generated: %s", err) 93 | log.Printf("warning: compile the package to analyze the error") 94 | src = buf 95 | } 96 | 97 | if err = ioutil.WriteFile(filename, src, 0644); err != nil { 98 | log.Fatalf("writing output [%s]: %v", filename, err) 99 | } 100 | } 101 | 102 | func tolower(s string) string { 103 | return strings.ToLower(s) 104 | } 105 | 106 | type structField struct { 107 | ModelTypeName string // name of the parent model object 108 | ModelPluralTypeName string // plural name of the parent model object 109 | Name string // name of the field 110 | } 111 | 112 | type typeGenerator struct { 113 | ModelTypeName string // the name of the model type 114 | ModelPluralTypeName string // the plural of the model type 115 | IDFieldName string // the field name for the ID 116 | SupportsAttributes bool // attributes can be supplied when listing 117 | SupportsPaging bool // paging is supported 118 | Fields []structField // fields on the struct we're generating for (converted to columns) 119 | } 120 | 121 | var modelTmpl = template.Must(template.New("modelTempl").Funcs(template.FuncMap{ 122 | "tolower": tolower, 123 | }).Parse(load("model.gotemplate"))) 124 | 125 | var specTmpl = template.Must(template.New("specTempl").Funcs(template.FuncMap{ 126 | "tolower": tolower, 127 | }).Parse(load("spec.gotemplate"))) 128 | 129 | func load(name string) string { 130 | _, file, _, _ := runtime.Caller(1) 131 | b, err := ioutil.ReadFile(filepath.Join(filepath.Dir(file), name)) 132 | if err != nil { 133 | log.Fatal(err) 134 | } 135 | return string(b) 136 | } 137 | -------------------------------------------------------------------------------- /generator/model.gotemplate: -------------------------------------------------------------------------------- 1 | // Code generated by go-uaa/generator; DO NOT EDIT. 2 | 3 | package uaa 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net/http"{{if .SupportsPaging}} 11 | "net/url" 12 | "strconv"{{end}} 13 | ) 14 | 15 | // Get{{.ModelTypeName}} with the given {{tolower .ModelTypeName}}ID. 16 | func (a *API) Get{{.ModelTypeName}}({{tolower .ModelTypeName}}ID string) (*{{.ModelTypeName}}, error) { 17 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", {{.ModelPluralTypeName}}Endpoint, {{tolower .ModelTypeName}}ID)) 18 | {{tolower .ModelTypeName}} := &{{.ModelTypeName}}{} 19 | err := a.doJSON(http.MethodGet, &u, nil, {{tolower .ModelTypeName}}, true) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return {{tolower .ModelTypeName}}, nil 24 | } 25 | 26 | // Create{{.ModelTypeName}} creates the given {{tolower .ModelTypeName}}. 27 | func (a *API) Create{{.ModelTypeName}}({{tolower .ModelTypeName}} {{.ModelTypeName}}) (*{{.ModelTypeName}}, error) { 28 | u := urlWithPath(*a.TargetURL, {{.ModelPluralTypeName}}Endpoint) 29 | created := &{{.ModelTypeName}}{} 30 | j, err := json.Marshal({{tolower .ModelTypeName}}) 31 | if err != nil { 32 | return nil, err 33 | } 34 | err = a.doJSON(http.MethodPost, &u, bytes.NewBuffer([]byte(j)), created, true) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return created, nil 39 | } 40 | 41 | // Update{{.ModelTypeName}} updates the given {{tolower .ModelTypeName}}. 42 | func (a *API) Update{{.ModelTypeName}}({{tolower .ModelTypeName}} {{.ModelTypeName}}) (*{{.ModelTypeName}}, error) { 43 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", {{.ModelPluralTypeName}}Endpoint, {{tolower .ModelTypeName}}.Identifier())) 44 | 45 | created := &{{.ModelTypeName}}{} 46 | j, err := json.Marshal({{tolower .ModelTypeName}}) 47 | if err != nil { 48 | return nil, err 49 | } 50 | err = a.doJSONWithHeaders(http.MethodPut, &u, map[string]string{"If-Match": "*"}, bytes.NewBuffer([]byte(j)), created, true) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return created, nil 55 | } 56 | 57 | // Delete{{.ModelTypeName}} deletes the {{tolower .ModelTypeName}} with the given {{tolower .ModelTypeName}} ID. 58 | func (a *API) Delete{{.ModelTypeName}}({{tolower .ModelTypeName}}ID string) (*{{.ModelTypeName}}, error) { 59 | if {{tolower .ModelTypeName}}ID == "" { 60 | return nil, errors.New("{{tolower .ModelTypeName}}ID cannot be blank") 61 | } 62 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", {{.ModelPluralTypeName}}Endpoint, {{tolower .ModelTypeName}}ID)) 63 | deleted := &{{.ModelTypeName}}{} 64 | err := a.doJSON(http.MethodDelete, &u, nil, deleted, true) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return deleted, nil 69 | } 70 | 71 | {{if .SupportsPaging}}// List{{.ModelPluralTypeName}} with the given filter, sortBy, attributes, sortOrder, startIndex 72 | // (1-based), and count (default 100). 73 | // If successful, List{{.ModelPluralTypeName}} returns the {{tolower .ModelPluralTypeName}} and the total itemsPerPage of {{tolower .ModelPluralTypeName}} for 74 | // all pages. If unsuccessful, List{{.ModelPluralTypeName}} returns the error. 75 | func (a *API) List{{.ModelPluralTypeName}}(filter string, sortBy string{{if .SupportsAttributes}}, attributes string{{end}}, sortOrder SortOrder, startIndex int, itemsPerPage int) ([]{{.ModelTypeName}}, Page, error) { 76 | u := urlWithPath(*a.TargetURL, {{.ModelPluralTypeName}}Endpoint) 77 | query := url.Values{} 78 | if filter != "" { 79 | query.Set("filter", filter) 80 | } 81 | {{if .SupportsAttributes}}if attributes != "" { 82 | query.Set("attributes", attributes) 83 | } 84 | {{end}}if sortBy != "" { 85 | query.Set("sortBy", sortBy) 86 | } 87 | if sortOrder != "" { 88 | query.Set("sortOrder", string(sortOrder)) 89 | } 90 | if startIndex == 0 { 91 | startIndex = 1 92 | } 93 | query.Set("startIndex", strconv.Itoa(startIndex)) 94 | if itemsPerPage == 0 { 95 | itemsPerPage = 100 96 | } 97 | query.Set("count", strconv.Itoa(itemsPerPage)) 98 | u.RawQuery = query.Encode() 99 | 100 | {{tolower .ModelPluralTypeName}} := &paginated{{.ModelTypeName}}List{} 101 | err := a.doJSON(http.MethodGet, &u, nil, {{tolower .ModelPluralTypeName}}, true) 102 | if err != nil { 103 | return nil, Page{}, err 104 | } 105 | page := Page{ 106 | StartIndex: {{tolower .ModelPluralTypeName}}.StartIndex, 107 | ItemsPerPage: {{tolower .ModelPluralTypeName}}.ItemsPerPage, 108 | TotalResults: {{tolower .ModelPluralTypeName}}.TotalResults, 109 | } 110 | return {{tolower .ModelPluralTypeName}}.Resources, page, err 111 | } 112 | 113 | // ListAll{{.ModelPluralTypeName}} retrieves UAA {{tolower .ModelPluralTypeName}} 114 | func (a *API) ListAll{{.ModelPluralTypeName}}(filter string, sortBy string{{if .SupportsAttributes}}, attributes string{{end}}, sortOrder SortOrder) ([]{{.ModelTypeName}}, error) { 115 | page := Page{ 116 | StartIndex: 1, 117 | ItemsPerPage: 100, 118 | } 119 | var ( 120 | results []{{.ModelTypeName}} 121 | currentPage []{{.ModelTypeName}} 122 | err error 123 | ) 124 | 125 | for { 126 | currentPage, page, err = a.List{{.ModelPluralTypeName}}(filter, sortBy{{if .SupportsAttributes}}, attributes{{end}}, sortOrder, page.StartIndex, page.ItemsPerPage) 127 | if err != nil { 128 | return nil, err 129 | } 130 | results = append(results, currentPage...) 131 | 132 | if (page.StartIndex + page.ItemsPerPage) > page.TotalResults { 133 | break 134 | } 135 | page.StartIndex = page.StartIndex + page.ItemsPerPage 136 | } 137 | return results, nil 138 | }{{else}}// List{{.ModelPluralTypeName}} fetches all of the {{.ModelTypeName}} records. 139 | // If successful, List{{.ModelPluralTypeName}} returns the {{tolower .ModelPluralTypeName}} 140 | // If unsuccessful, List{{.ModelPluralTypeName}} returns the error. 141 | func (a *API) List{{.ModelPluralTypeName}}() ([]{{.ModelTypeName}}, error) { 142 | u := urlWithPath(*a.TargetURL, {{.ModelPluralTypeName}}Endpoint) 143 | var {{tolower .ModelPluralTypeName}} []{{.ModelTypeName}} 144 | err := a.doJSON(http.MethodGet, &u, nil, &{{tolower .ModelPluralTypeName}}, true) 145 | if err != nil { 146 | return nil, err 147 | } 148 | return {{tolower .ModelPluralTypeName}}, nil 149 | }{{end}} 150 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudfoundry-community/go-uaa 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/onsi/ginkgo/v2 v2.23.4 9 | github.com/onsi/gomega v1.37.0 10 | github.com/pkg/errors v0.9.1 11 | github.com/sclevine/spec v1.4.0 12 | golang.org/x/oauth2 v0.30.0 13 | ) 14 | 15 | require ( 16 | github.com/go-logr/logr v1.4.2 // indirect 17 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 18 | github.com/google/go-cmp v0.7.0 // indirect 19 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 20 | go.uber.org/automaxprocs v1.6.0 // indirect 21 | golang.org/x/net v0.38.0 // indirect 22 | golang.org/x/sys v0.32.0 // indirect 23 | golang.org/x/text v0.23.0 // indirect 24 | golang.org/x/tools v0.31.0 // indirect 25 | google.golang.org/protobuf v1.36.5 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 4 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 5 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 6 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 7 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 8 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 9 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 10 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 11 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 12 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 16 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 17 | github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 18 | github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 19 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 24 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 25 | github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= 26 | github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= 27 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 28 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 29 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 30 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 31 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 32 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 33 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 34 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 35 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 36 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 37 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 38 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 39 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 40 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 41 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 42 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 45 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 47 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 48 | -------------------------------------------------------------------------------- /go_uaa_suite_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestGoUaa(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "GoUaa Suite") 13 | } 14 | -------------------------------------------------------------------------------- /groups.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | ) 12 | 13 | // GroupsEndpoint is the path to the groups resource. 14 | const GroupsEndpoint string = "/Groups" 15 | 16 | // paginatedGroupList is the response from the API for a single page of groups. 17 | type paginatedGroupList struct { 18 | Page 19 | Resources []Group `json:"resources"` 20 | Schemas []string `json:"schemas"` 21 | } 22 | 23 | // GroupMember is a user or a group. 24 | type GroupMember struct { 25 | Origin string `json:"origin,omitempty"` 26 | Type string `json:"type,omitempty"` 27 | Value string `json:"value,omitempty"` 28 | } 29 | 30 | // Group is a container for users and groups. 31 | type Group struct { 32 | ID string `json:"id,omitempty"` 33 | Meta *Meta `json:"meta,omitempty"` 34 | DisplayName string `json:"displayName,omitempty"` 35 | ZoneID string `json:"zoneId,omitempty"` 36 | Description string `json:"description,omitempty"` 37 | Members []GroupMember `json:"members,omitempty"` 38 | Schemas []string `json:"schemas,omitempty"` 39 | } 40 | 41 | // paginatedGroupMappingList is the response from the API for a single page of group mappings. 42 | type paginatedGroupMappingList struct { 43 | Page 44 | Resources []GroupMapping `json:"resources"` 45 | Schemas []string `json:"schemas"` 46 | } 47 | 48 | // GroupMapping is a container for external group mapping 49 | type GroupMapping struct { 50 | GroupID string `json:"groupId,omitempty"` 51 | DisplayName string `json:"displayName,omitempty"` 52 | ExternalGroup string `json:"externalGroup,omitempty"` 53 | Origin string `json:"origin,omitempty"` 54 | Meta *Meta `json:"meta,omitempty"` 55 | Schemas []string `json:"schemas,omitempty"` 56 | } 57 | 58 | // Identifier returns the field used to uniquely identify a Group. 59 | func (g Group) Identifier() string { 60 | return g.ID 61 | } 62 | 63 | // AddGroupMember adds the entity with the given memberID to the group with the 64 | // given ID. If no entityType is supplied, the entityType (which can be "USER" 65 | // or "GROUP") will be "USER". If no origin is supplied, the origin will be 66 | // "uaa". 67 | func (a *API) AddGroupMember(groupID string, memberID string, entityType string, origin string) error { 68 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s/members", GroupsEndpoint, groupID)) 69 | if origin == "" { 70 | origin = "uaa" 71 | } 72 | if entityType == "" { 73 | entityType = "USER" 74 | } 75 | membership := GroupMember{Origin: origin, Type: entityType, Value: memberID} 76 | j, err := json.Marshal(membership) 77 | if err != nil { 78 | return err 79 | } 80 | err = a.doJSON(http.MethodPost, &u, bytes.NewBuffer([]byte(j)), nil, true) 81 | if err != nil { 82 | return err 83 | } 84 | return nil 85 | } 86 | 87 | // RemoveGroupMember removes the entity with the given memberID from the group 88 | // with the given ID. If no entityType is supplied, the entityType (which can be 89 | // "USER" or "GROUP") will be "USER". If no origin is supplied, the origin will 90 | // be "uaa". 91 | func (a *API) RemoveGroupMember(groupID string, memberID string, entityType string, origin string) error { 92 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s/members/%s", GroupsEndpoint, groupID, memberID)) 93 | if origin == "" { 94 | origin = "uaa" 95 | } 96 | if entityType == "" { 97 | entityType = "USER" 98 | } 99 | membership := GroupMember{Origin: origin, Type: entityType, Value: memberID} 100 | j, err := json.Marshal(membership) 101 | if err != nil { 102 | return err 103 | } 104 | err = a.doJSON(http.MethodDelete, &u, bytes.NewBuffer([]byte(j)), nil, true) 105 | if err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | // GetGroupByName gets the group with the given name 112 | // http://docs.cloudfoundry.org/api/uaa/version/4.14.0/index.html#list-4. 113 | func (a *API) GetGroupByName(name string, attributes string) (*Group, error) { 114 | if name == "" { 115 | return nil, errors.New("group name may not be blank") 116 | } 117 | 118 | filter := fmt.Sprintf(`displayName eq "%v"`, name) 119 | groups, err := a.ListAllGroups(filter, "", attributes, "") 120 | if err != nil { 121 | return nil, err 122 | } 123 | if len(groups) == 0 { 124 | return nil, fmt.Errorf("group %v not found", name) 125 | } 126 | return &groups[0], nil 127 | } 128 | 129 | func (a *API) MapGroup(groupID string, externalGroup string, origin string) error { 130 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/External", GroupsEndpoint)) 131 | if origin == "" { 132 | origin = "ldap" 133 | } 134 | mapped := &GroupMapping{} 135 | mapping := GroupMapping{Origin: origin, GroupID: groupID, ExternalGroup: externalGroup} 136 | j, err := json.Marshal(mapping) 137 | if err != nil { 138 | return err 139 | } 140 | err = a.doJSON(http.MethodPost, &u, bytes.NewBuffer([]byte(j)), mapped, true) 141 | if err != nil { 142 | return err 143 | } 144 | return nil 145 | } 146 | 147 | func (a *API) UnmapGroup(groupID string, externalGroup string, origin string) error { 148 | if origin == "" { 149 | origin = "ldap" 150 | } 151 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/External/groupId/%s/externalGroup/%s/origin/%s", GroupsEndpoint, groupID, externalGroup, origin)) 152 | mapped := &GroupMapping{} 153 | err := a.doJSON(http.MethodDelete, &u, nil, mapped, true) 154 | if err != nil { 155 | return err 156 | } 157 | return nil 158 | } 159 | 160 | func (a *API) ListGroupMappings(origin string, startIndex int, itemsPerPage int) ([]GroupMapping, Page, error) { 161 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/External", GroupsEndpoint)) 162 | query := url.Values{} 163 | if origin != "" { 164 | query.Set("origin", origin) 165 | } 166 | if startIndex == 0 { 167 | startIndex = 1 168 | } 169 | query.Set("startIndex", strconv.Itoa(startIndex)) 170 | if itemsPerPage == 0 { 171 | itemsPerPage = 100 172 | } 173 | query.Set("count", strconv.Itoa(itemsPerPage)) 174 | u.RawQuery = query.Encode() 175 | 176 | mappings := &paginatedGroupMappingList{} 177 | err := a.doJSON(http.MethodGet, &u, nil, mappings, true) 178 | if err != nil { 179 | return nil, Page{}, err 180 | } 181 | page := Page{ 182 | StartIndex: mappings.StartIndex, 183 | ItemsPerPage: mappings.ItemsPerPage, 184 | TotalResults: mappings.TotalResults, 185 | } 186 | return mappings.Resources, page, err 187 | } 188 | 189 | // ListAllGroups retrieves UAA groups 190 | func (a *API) ListAllGroupMappings(origin string) ([]GroupMapping, error) { 191 | page := Page{ 192 | StartIndex: 1, 193 | ItemsPerPage: 100, 194 | } 195 | var ( 196 | results []GroupMapping 197 | currentPage []GroupMapping 198 | err error 199 | ) 200 | 201 | for { 202 | currentPage, page, err = a.ListGroupMappings(origin, page.StartIndex, page.ItemsPerPage) 203 | if err != nil { 204 | return nil, err 205 | } 206 | results = append(results, currentPage...) 207 | 208 | if (page.StartIndex + page.ItemsPerPage) > page.TotalResults { 209 | break 210 | } 211 | page.StartIndex = page.StartIndex + page.ItemsPerPage 212 | } 213 | return results, nil 214 | } 215 | -------------------------------------------------------------------------------- /groups_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | uaa "github.com/cloudfoundry-community/go-uaa" 10 | 11 | . "github.com/onsi/gomega" 12 | "github.com/sclevine/spec" 13 | ) 14 | 15 | const groupResponse string = `{ 16 | "id" : "00000000-0000-0000-0000-000000000001", 17 | "meta" : { 18 | "version" : 1, 19 | "created" : "2017-01-15T16:54:15.677Z", 20 | "lastModified" : "2017-08-15T16:54:15.677Z" 21 | }, 22 | "displayName" : "cloud_controller.read", 23 | "description" : "View details of your applications and services", 24 | "members" : [ { 25 | "origin" : "uaa", 26 | "type" : "USER", 27 | "value" : "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70" 28 | } ], 29 | "zoneID" : "uaa", 30 | "schemas" : [ "urn:scim:schemas:core:1.0" ] 31 | }` 32 | 33 | var groupListResponse = fmt.Sprintf(PaginatedResponseTmpl, UaaAdminGroupResponse, CloudControllerReadGroupResponse) 34 | 35 | const CloudControllerReadGroupResponse string = `{ 36 | "id" : "00000000-0000-0000-0000-000000000002", 37 | "meta" : { 38 | "version" : 1, 39 | "created" : "2017-01-15T16:54:15.677Z", 40 | "lastModified" : "2017-08-15T16:54:15.677Z" 41 | }, 42 | "displayName" : "cloud_controller.read", 43 | "description" : "View details of your applications and services", 44 | "members" : [ { 45 | "origin" : "uaa", 46 | "type" : "USER", 47 | "value" : "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70" 48 | } ], 49 | "zoneID" : "uaa", 50 | "schemas" : [ "urn:scim:schemas:core:1.0" ] 51 | }` 52 | 53 | const UaaAdminGroupResponse string = `{ 54 | "id" : "00000000-0000-0000-0000-000000000001", 55 | "meta" : { 56 | "version" : 1, 57 | "created" : "2017-01-15T16:54:15.677Z", 58 | "lastModified" : "2017-08-15T16:54:15.677Z" 59 | }, 60 | "displayName" : "uaa.admin", 61 | "description" : "Act as an administrator throughout the UAA", 62 | "members" : [ { 63 | "origin" : "uaa", 64 | "type" : "USER", 65 | "value" : "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70" 66 | } ], 67 | "zoneID" : "uaa", 68 | "schemas" : [ "urn:scim:schemas:core:1.0" ] 69 | }` 70 | 71 | var testGroupValue uaa.Group = uaa.Group{ 72 | ID: "00000000-0000-0000-0000-000000000001", 73 | DisplayName: "uaa.admin", 74 | } 75 | 76 | const testGroupJSON string = `{ "id" : "00000000-0000-0000-0000-000000000001", "displayName": "uaa.admin" }` 77 | 78 | func testGroupsExtra(t *testing.T, when spec.G, it spec.S) { 79 | var ( 80 | s *httptest.Server 81 | handler http.Handler 82 | called int 83 | a *uaa.API 84 | ) 85 | 86 | it.Before(func() { 87 | RegisterTestingT(t) 88 | called = 0 89 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 90 | called = called + 1 91 | Expect(handler).NotTo(BeNil()) 92 | handler.ServeHTTP(w, req) 93 | })) 94 | a, _ = uaa.New(s.URL, uaa.WithNoAuthentication()) 95 | }) 96 | 97 | it.After(func() { 98 | if s != nil { 99 | s.Close() 100 | } 101 | }) 102 | 103 | when("GetGroupByName()", func() { 104 | when("when no group name is specified", func() { 105 | it("returns an error", func() { 106 | _, err := a.GetGroupByName("", "") 107 | Expect(err).To(HaveOccurred()) 108 | Expect(err.Error()).To(Equal("group name may not be blank")) 109 | }) 110 | }) 111 | 112 | when("when no origin is specified", func() { 113 | it("looks up a group with a SCIM filter", func() { 114 | group := uaa.Group{DisplayName: "uaa.admin"} 115 | response := PaginatedResponse(group) 116 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 117 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 118 | Expect(req.URL.Path).To(Equal(uaa.GroupsEndpoint)) 119 | Expect(req.URL.Query().Get("filter")).To(Equal(`displayName eq "uaa.admin"`)) 120 | w.WriteHeader(http.StatusOK) 121 | _, err := w.Write([]byte(response)) 122 | Expect(err).NotTo(HaveOccurred()) 123 | }) 124 | 125 | g, err := a.GetGroupByName("uaa.admin", "") 126 | Expect(err).NotTo(HaveOccurred()) 127 | Expect(g.DisplayName).To(Equal("uaa.admin")) 128 | }) 129 | 130 | it("returns an error when request fails", func() { 131 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 132 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 133 | Expect(req.URL.Path).To(Equal(uaa.GroupsEndpoint)) 134 | Expect(req.URL.Query().Get("filter")).To(Equal(`displayName eq "uaa.admin"`)) 135 | w.WriteHeader(http.StatusInternalServerError) 136 | }) 137 | 138 | _, err := a.GetGroupByName("uaa.admin", "") 139 | Expect(err).To(HaveOccurred()) 140 | Expect(err.Error()).To(ContainSubstring("An error")) 141 | }) 142 | 143 | it("returns an error when no groups are found", func() { 144 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 145 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 146 | Expect(req.URL.Path).To(Equal(uaa.GroupsEndpoint)) 147 | Expect(req.URL.Query().Get("filter")).To(Equal(`displayName eq "uaa.admin"`)) 148 | w.WriteHeader(http.StatusOK) 149 | _, err := w.Write([]byte(PaginatedResponse())) 150 | Expect(err).NotTo(HaveOccurred()) 151 | }) 152 | 153 | _, err := a.GetGroupByName("uaa.admin", "") 154 | Expect(err).To(HaveOccurred()) 155 | Expect(err.Error()).To(Equal(`group uaa.admin not found`)) 156 | }) 157 | }) 158 | 159 | when("when attributes are specified", func() { 160 | it("adds them to the GET request", func() { 161 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 162 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 163 | Expect(req.URL.Path).To(Equal(uaa.GroupsEndpoint)) 164 | Expect(req.URL.Query().Get("filter")).To(Equal(`displayName eq "uaa.admin"`)) 165 | Expect(req.URL.Query().Get("attributes")).To(Equal(`displayName`)) 166 | w.WriteHeader(http.StatusOK) 167 | _, err := w.Write([]byte(PaginatedResponse(uaa.Group{DisplayName: "uaa.admin"}))) 168 | Expect(err).NotTo(HaveOccurred()) 169 | }) 170 | _, err := a.GetGroupByName("uaa.admin", "displayName") 171 | Expect(err).NotTo(HaveOccurred()) 172 | }) 173 | }) 174 | }) 175 | 176 | when("AddGroupMember()", func() { 177 | it("adds a membership", func() { 178 | membershipJSON := `{"origin":"uaa","type":"USER","value":"user-id-1"}` 179 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 180 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 181 | Expect(req.URL.Path).To(Equal(fmt.Sprintf("%s/%s/members", uaa.GroupsEndpoint, "group-id-1"))) 182 | w.WriteHeader(http.StatusOK) 183 | _, err := w.Write([]byte(membershipJSON)) 184 | Expect(err).NotTo(HaveOccurred()) 185 | }) 186 | err := a.AddGroupMember("group-id-1", "user-id-1", "", "") 187 | Expect(err).NotTo(HaveOccurred()) 188 | Expect(called).To(Equal(1)) 189 | }) 190 | }) 191 | 192 | when("RemoveGroupMember", func() { 193 | it("removes a membership", func() { 194 | membershipJSON := `{"origin":"uaa","type":"USER","value":"user-id-1"}` 195 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 196 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 197 | Expect(req.URL.Path).To(Equal(fmt.Sprintf("%s/%s/members", uaa.GroupsEndpoint, "group-id-1"))) 198 | w.WriteHeader(http.StatusOK) 199 | _, err := w.Write([]byte(membershipJSON)) 200 | Expect(err).NotTo(HaveOccurred()) 201 | }) 202 | err := a.AddGroupMember("group-id-1", "user-id-1", "", "") 203 | Expect(err).NotTo(HaveOccurred()) 204 | Expect(called).To(Equal(1)) 205 | 206 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 207 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 208 | Expect(req.URL.Path).To(Equal(fmt.Sprintf("%s/%s/members/%s", uaa.GroupsEndpoint, "group-id-1", "user-id-1"))) 209 | w.WriteHeader(http.StatusOK) 210 | _, err := w.Write([]byte(membershipJSON)) 211 | Expect(err).NotTo(HaveOccurred()) 212 | }) 213 | err = a.RemoveGroupMember("group-id-1", "user-id-1", "", "") 214 | Expect(err).NotTo(HaveOccurred()) 215 | Expect(called).To(Equal(2)) 216 | }) 217 | }) 218 | } 219 | -------------------------------------------------------------------------------- /health.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | // IsHealthy returns true if the UAA is healthy, false if it is unhealthy, and 4 | // an error if there is an issue making a request to the /healthz endpoint. 5 | func (a *API) IsHealthy() (bool, error) { 6 | u := urlWithPath(*a.TargetURL, "/healthz") 7 | resp, err := a.Client.Get(u.String()) 8 | if err != nil { 9 | return false, err 10 | } 11 | if resp.StatusCode == 200 { 12 | return true, nil 13 | } 14 | 15 | return false, nil 16 | } 17 | -------------------------------------------------------------------------------- /health_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | uaa "github.com/cloudfoundry-community/go-uaa" 9 | . "github.com/onsi/gomega" 10 | "github.com/sclevine/spec" 11 | ) 12 | 13 | func testIsHealthy(t *testing.T, when spec.G, it spec.S) { 14 | var ( 15 | s *httptest.Server 16 | handler http.Handler 17 | called int 18 | a *uaa.API 19 | ) 20 | 21 | it.Before(func() { 22 | RegisterTestingT(t) 23 | called = 0 24 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 25 | called = called + 1 26 | Expect(handler).NotTo(BeNil()) 27 | handler.ServeHTTP(w, req) 28 | })) 29 | a, _ = uaa.New(s.URL, uaa.WithNoAuthentication()) 30 | }) 31 | 32 | it.After(func() { 33 | if s != nil { 34 | s.Close() 35 | } 36 | }) 37 | 38 | it("is healthy when a 200 response is received", func() { 39 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 40 | Expect(req.URL.Path).To(Equal("/healthz")) 41 | w.WriteHeader(http.StatusOK) 42 | _, err := w.Write([]byte("ok")) 43 | Expect(err).NotTo(HaveOccurred()) 44 | }) 45 | 46 | status, err := a.IsHealthy() 47 | Expect(status).To(BeTrue()) 48 | Expect(err).NotTo(HaveOccurred()) 49 | }) 50 | 51 | it("is unhealthy when a non-200 response is received", func() { 52 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 53 | Expect(req.URL.Path).To(Equal("/healthz")) 54 | w.WriteHeader(http.StatusInternalServerError) 55 | _, err := w.Write([]byte("ok")) 56 | Expect(err).NotTo(HaveOccurred()) 57 | }) 58 | status, err := a.IsHealthy() 59 | Expect(status).To(BeFalse()) 60 | Expect(err).NotTo(HaveOccurred()) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /identity_zones.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | // IdentityZonesEndpoint is the path to the users resource. 4 | const IdentityZonesEndpoint string = "/identity-zones" 5 | 6 | // IdentityZone is a UAA identity zone. 7 | // http://docs.cloudfoundry.org/api/uaa/version/4.14.0/index.html#identity-zones 8 | type IdentityZone struct { 9 | ID string `json:"id,omitempty"` 10 | Subdomain string `json:"subdomain"` 11 | Config IdentityZoneConfig `json:"config"` 12 | Name string `json:"name"` 13 | Version int `json:"version,omitempty"` 14 | Description string `json:"description,omitempty"` 15 | Created int `json:"created,omitempty"` 16 | LastModified int `json:"last_modified,omitempty"` 17 | } 18 | 19 | // Identifier returns the field used to uniquely identify an IdentityZone. 20 | func (iz IdentityZone) Identifier() string { 21 | return iz.ID 22 | } 23 | 24 | // ClientSecretPolicy is an identity zone client secret policy. 25 | type ClientSecretPolicy struct { 26 | MinLength int `json:"minLength,omitempty"` 27 | MaxLength int `json:"maxLength,omitempty"` 28 | RequireUpperCaseCharacter int `json:"requireUpperCaseCharacter,omitempty"` 29 | RequireLowerCaseCharacter int `json:"requireLowerCaseCharacter,omitempty"` 30 | RequireDigit int `json:"requireDigit,omitempty"` 31 | RequireSpecialCharacter int `json:"requireSpecialCharacter,omitempty"` 32 | } 33 | 34 | // TokenPolicy is an identity zone token policy. 35 | type TokenPolicy struct { 36 | AccessTokenValidity int `json:"accessTokenValidity,omitempty"` 37 | RefreshTokenValidity int `json:"refreshTokenValidity,omitempty"` 38 | JWTRevocable bool `json:"jwtRevocable,omitempty"` 39 | RefreshTokenUnique bool `json:"refreshTokenUnique,omitempty"` 40 | RefreshTokenFormat string `json:"refreshTokenFormat,omitempty"` 41 | ActiveKeyID string `json:"activeKeyId,omitempty"` 42 | } 43 | 44 | // SAMLKey is an identity zone SAML key. 45 | type SAMLKey struct { 46 | Key string `json:"key,omitempty"` 47 | Passphrase string `json:"passphrase,omitempty"` 48 | Certificate string `json:"certificate,omitempty"` 49 | } 50 | 51 | // SAMLConfig is an identity zone SAMLConfig. 52 | type SAMLConfig struct { 53 | AssertionSigned bool `json:"assertionSigned,omitempty"` 54 | RequestSigned bool `json:"requestSigned,omitempty"` 55 | WantAssertionSigned bool `json:"wantAssertionSigned,omitempty"` 56 | WantAuthnRequestSigned bool `json:"wantAuthnRequestSigned,omitempty"` 57 | AssertionTimeToLiveSeconds int `json:"assertionTimeToLiveSeconds,omitempty"` 58 | ActiveKeyID string `json:"activeKeyId,omitempty"` 59 | Keys map[string]SAMLKey `json:"keys,omitempty"` 60 | DisableInResponseToCheck bool `json:"disableInResponseToCheck,omitempty"` 61 | } 62 | 63 | // CORSPolicy is an identity zone CORSPolicy. 64 | type CORSPolicy struct { 65 | XHRConfiguration struct { 66 | AllowedOrigins []string `json:"allowedOrigins,omitempty"` 67 | AllowedOriginPatterns []interface{} `json:"allowedOriginPatterns,omitempty"` 68 | AllowedURIs []string `json:"allowedUris,omitempty"` 69 | AllowedURIPatterns []interface{} `json:"allowedUriPatterns,omitempty"` 70 | AllowedHeaders []string `json:"allowedHeaders,omitempty"` 71 | AllowedMethods []string `json:"allowedMethods,omitempty"` 72 | AllowedCredentials bool `json:"allowedCredentials,omitempty"` 73 | MaxAge int `json:"maxAge,omitempty"` 74 | } `json:"xhrConfiguration,omitempty"` 75 | DefaultConfiguration struct { 76 | AllowedOrigins []string `json:"allowedOrigins,omitempty"` 77 | AllowedOriginPatterns []interface{} `json:"allowedOriginPatterns,omitempty"` 78 | AllowedURIs []string `json:"allowedUris,omitempty"` 79 | AllowedURIPatterns []interface{} `json:"allowedUriPatterns,omitempty"` 80 | AllowedHeaders []string `json:"allowedHeaders,omitempty"` 81 | AllowedMethods []string `json:"allowedMethods,omitempty"` 82 | AllowedCredentials bool `json:"allowedCredentials,omitempty"` 83 | MaxAge int `json:"maxAge,omitempty"` 84 | } `json:"defaultConfiguration,omitempty"` 85 | } 86 | 87 | // IdentityZoneLinks is an identity zone link. 88 | type IdentityZoneLinks struct { 89 | Logout struct { 90 | RedirectURL string `json:"redirectUrl,omitempty"` 91 | RedirectParameterName string `json:"redirectParameterName,omitempty"` 92 | DisableRedirectParameter bool `json:"disableRedirectParameter,omitempty"` 93 | Whitelist []string `json:"whitelist,omitempty"` 94 | } `json:"logout,omitempty"` 95 | HomeRedirect string `json:"homeRedirect,omitempty"` 96 | SelfService struct { 97 | SelfServiceLinksEnabled bool `json:"selfServiceLinksEnabled,omitempty"` 98 | Signup string `json:"signup,omitempty"` 99 | Passwd string `json:"passwd,omitempty"` 100 | } `json:"selfService,omitempty"` 101 | } 102 | 103 | // Prompt is a UAA prompt. 104 | type Prompt struct { 105 | Name string `json:"name,omitempty"` 106 | Type string `json:"type,omitempty"` 107 | Text string `json:"text,omitempty"` 108 | } 109 | 110 | // Branding is the branding for a UAA identity zone. 111 | type Branding struct { 112 | CompanyName string `json:"companyName,omitempty"` 113 | ProductLogo string `json:"productLogo,omitempty"` 114 | SquareLogo string `json:"squareLogo,omitempty"` 115 | } 116 | 117 | // IdentityZoneUserConfig is the user configuration for an identity zone. 118 | type IdentityZoneUserConfig struct { 119 | DefaultGroups []string `json:"defaultGroups,omitempty"` 120 | } 121 | 122 | // IdentityZoneMFAConfig is the MFA configuration for an identity zone. 123 | type IdentityZoneMFAConfig struct { 124 | Enabled *bool `json:"enabled,omitempty"` 125 | ProviderName string `json:"providerName,omitempty"` 126 | } 127 | 128 | // IdentityZoneConfig is the configuration for an identity zone. 129 | type IdentityZoneConfig struct { 130 | ClientSecretPolicy *ClientSecretPolicy `json:"clientSecretPolicy,omitempty"` 131 | TokenPolicy *TokenPolicy `json:"tokenPolicy,omitempty"` 132 | SAMLConfig *SAMLConfig `json:"samlConfig,omitempty"` 133 | CORSPolicy *CORSPolicy `json:"corsPolicy,omitempty"` 134 | Links *IdentityZoneLinks `json:"links,omitempty"` 135 | Prompts []Prompt `json:"prompts,omitempty"` 136 | IDPDiscoveryEnabled *bool `json:"idpDiscoveryEnabled,omitempty"` 137 | Branding *Branding `json:"branding,omitempty"` 138 | AccountChooserEnabled *bool `json:"accountChooserEnabled,omitempty"` 139 | UserConfig *IdentityZoneUserConfig `json:"userConfig,omitempty"` 140 | MFAConfig *IdentityZoneMFAConfig `json:"mfaConfig,omitempty"` 141 | } 142 | -------------------------------------------------------------------------------- /info.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Info is information about the UAA server. 8 | type Info struct { 9 | App uaaApp `json:"app"` 10 | Links uaaLinks `json:"links"` 11 | Prompts map[string][]string `json:"prompts"` 12 | ZoneName string `json:"zone_name"` 13 | EntityID string `json:"entityID"` 14 | CommitID string `json:"commit_id"` 15 | Timestamp string `json:"timestamp"` 16 | IdpDefinitions map[string]string `json:"idpDefinitions"` 17 | } 18 | 19 | type uaaApp struct { 20 | Version string `json:"version"` 21 | } 22 | 23 | type uaaLinks struct { 24 | ForgotPassword string `json:"passwd"` 25 | Uaa string `json:"uaa"` 26 | Registration string `json:"register"` 27 | Login string `json:"login"` 28 | } 29 | 30 | // GetInfo gets server information 31 | // http://docs.cloudfoundry.org/api/uaa/version/4.14.0/index.html#server-information-2. 32 | func (a *API) GetInfo() (*Info, error) { 33 | url := urlWithPath(*a.TargetURL, "/info") 34 | 35 | info := &Info{} 36 | err := a.doJSON(http.MethodGet, &url, nil, info, false) 37 | return info, err 38 | } 39 | -------------------------------------------------------------------------------- /info_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | 7 | "testing" 8 | 9 | uaa "github.com/cloudfoundry-community/go-uaa" 10 | . "github.com/onsi/gomega" 11 | 12 | "github.com/sclevine/spec" 13 | ) 14 | 15 | const InfoResponseJSON string = `{ 16 | "app": { 17 | "version": "4.5.0" 18 | }, 19 | "links": { 20 | "uaa": "https://uaa.run.pivotal.io", 21 | "passwd": "https://account.run.pivotal.io/forgot-password", 22 | "login": "https://login.run.pivotal.io", 23 | "register": "https://account.run.pivotal.io/sign-up" 24 | }, 25 | "zone_name": "uaa", 26 | "entityID": "login.run.pivotal.io", 27 | "commit_id": "df80f63", 28 | "idpDefinitions": { 29 | "SAML" : "http://localhost:8080/uaa/saml/discovery?returnIDParam=idp&entityID=cloudfoundry-saml-login&idp=SAML&isPassive=true" 30 | }, 31 | "prompts": { 32 | "username": [ 33 | "text", 34 | "Email" 35 | ], 36 | "password": [ 37 | "password", 38 | "Password" 39 | ] 40 | }, 41 | "timestamp": "2017-07-21T22:45:01+0000" 42 | }` 43 | 44 | func testInfo(t *testing.T, when spec.G, it spec.S) { 45 | var ( 46 | s *httptest.Server 47 | a *uaa.API 48 | h http.Handler 49 | handlerCalls int 50 | ) 51 | 52 | it.Before(func() { 53 | RegisterTestingT(t) 54 | handlerCalls = 0 55 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 56 | handlerCalls = handlerCalls + 1 57 | h.ServeHTTP(w, req) 58 | })) 59 | a, _ = uaa.New(s.URL, uaa.WithNoAuthentication()) 60 | }) 61 | 62 | it.After(func() { 63 | if s != nil { 64 | s.Close() 65 | } 66 | }) 67 | 68 | when("the info endpoint responds with valid info", func() { 69 | it.Before(func() { 70 | h = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 71 | Expect(req.Method).To(Equal(http.MethodGet)) 72 | Expect(req.URL.Path).To(Equal("/info")) 73 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 74 | _, err := w.Write([]byte(InfoResponseJSON)) 75 | Expect(err).NotTo(HaveOccurred()) 76 | }) 77 | }) 78 | 79 | it("calls the /info endpoint", func() { 80 | infoResponse, _ := a.GetInfo() 81 | Expect(handlerCalls).To(Equal(1)) 82 | Expect(infoResponse.App.Version).To(Equal("4.5.0")) 83 | Expect(infoResponse.Links.ForgotPassword).To(Equal("https://account.run.pivotal.io/forgot-password")) 84 | Expect(infoResponse.Links.Uaa).To(Equal("https://uaa.run.pivotal.io")) 85 | Expect(infoResponse.Links.Registration).To(Equal("https://account.run.pivotal.io/sign-up")) 86 | Expect(infoResponse.Links.Login).To(Equal("https://login.run.pivotal.io")) 87 | Expect(infoResponse.ZoneName).To(Equal("uaa")) 88 | Expect(infoResponse.EntityID).To(Equal("login.run.pivotal.io")) 89 | Expect(infoResponse.CommitID).To(Equal("df80f63")) 90 | Expect(infoResponse.IdpDefinitions["SAML"]).To(Equal("http://localhost:8080/uaa/saml/discovery?returnIDParam=idp&entityID=cloudfoundry-saml-login&idp=SAML&isPassive=true")) 91 | Expect(infoResponse.Prompts["username"]).To(Equal([]string{"text", "Email"})) 92 | Expect(infoResponse.Prompts["password"]).To(Equal([]string{"password", "Password"})) 93 | Expect(infoResponse.Timestamp).To(Equal("2017-07-21T22:45:01+0000")) 94 | }) 95 | }) 96 | 97 | when("the info endpoint responds with an error", func() { 98 | it.Before(func() { 99 | h = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 100 | Expect(req.Method).To(Equal(http.MethodGet)) 101 | Expect(req.URL.Path).To(Equal("/info")) 102 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 103 | w.WriteHeader(http.StatusInternalServerError) 104 | }) 105 | }) 106 | 107 | it("returns a helpful error", func() { 108 | _, err := a.GetInfo() 109 | Expect(err).NotTo(BeNil()) 110 | Expect(err.Error()).To(ContainSubstring("An error occurred while calling")) 111 | }) 112 | }) 113 | 114 | when("the info endpoint responds with unparsable JSON", func() { 115 | it.Before(func() { 116 | h = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 117 | Expect(req.Method).To(Equal(http.MethodGet)) 118 | Expect(req.URL.Path).To(Equal("/info")) 119 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 120 | _, err := w.Write([]byte("{unparsable-json-response}")) 121 | Expect(err).NotTo(HaveOccurred()) 122 | }) 123 | }) 124 | 125 | it("returns a helpful error", func() { 126 | _, err := a.GetInfo() 127 | Expect(err).NotTo(BeNil()) 128 | Expect(err.Error()).To(ContainSubstring("An unknown error occurred while parsing response from")) 129 | Expect(err.Error()).To(ContainSubstring("Response was {unparsable-json-response}")) 130 | }) 131 | }) 132 | } 133 | -------------------------------------------------------------------------------- /issuer.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type OpenIDConfig struct { 8 | Issuer string `json:"issuer"` 9 | } 10 | 11 | // Issuer retrieves an issuer name from openid configuration 12 | func (a *API) Issuer() (string, error) { 13 | url := urlWithPath(*a.TargetURL, "/.well-known/openid-configuration") 14 | 15 | config := &OpenIDConfig{} 16 | err := a.doJSON(http.MethodGet, &url, nil, config, false) 17 | if err != nil { 18 | return "", err 19 | } 20 | return config.Issuer, nil 21 | } 22 | -------------------------------------------------------------------------------- /issuer_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/cloudfoundry-community/go-uaa" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/ghttp" 10 | ) 11 | 12 | var _ = Describe("Issuer", func() { 13 | var ( 14 | server *ghttp.Server 15 | api *uaa.API 16 | ) 17 | 18 | BeforeEach(func() { 19 | server = ghttp.NewServer() 20 | server.AppendHandlers(ghttp.CombineHandlers( 21 | ghttp.VerifyRequest("GET", "/.well-known/openid-configuration"), 22 | ghttp.RespondWithJSONEncoded(http.StatusOK, &uaa.OpenIDConfig{Issuer: "issuer"}), 23 | )) 24 | 25 | target := server.URL() 26 | 27 | var err error 28 | api, err = uaa.New(target, uaa.WithNoAuthentication()) 29 | Expect(err).NotTo(HaveOccurred()) 30 | }) 31 | 32 | AfterEach(func() { 33 | server.Close() 34 | }) 35 | 36 | It("return the issuer", func() { 37 | issuer, err := api.Issuer() 38 | Expect(err).NotTo(HaveOccurred()) 39 | Expect(issuer).To(Equal("issuer")) 40 | }) 41 | 42 | Context("when the server returns a non-200 status code", func() { 43 | BeforeEach(func() { 44 | server.Reset() 45 | server.AppendHandlers( 46 | ghttp.VerifyRequest("GET", "/.well-known/openid-configuration"), 47 | ghttp.RespondWith(http.StatusInternalServerError, nil), 48 | ) 49 | }) 50 | 51 | It("returns an error", func() { 52 | issuer, err := api.Issuer() 53 | Expect(err).To(HaveOccurred()) 54 | Expect(issuer).To(Equal("")) 55 | }) 56 | }) 57 | 58 | }) 59 | -------------------------------------------------------------------------------- /me.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // UserInfo is a protected resource required for OpenID Connect compatibility. 8 | // The response format is defined here: https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse. 9 | type UserInfo struct { 10 | UserID string `json:"user_id"` 11 | Sub string `json:"sub"` 12 | Username string `json:"user_name"` 13 | GivenName string `json:"given_name"` 14 | FamilyName string `json:"family_name"` 15 | Email string `json:"email"` 16 | PhoneNumber string `json:"phone_number"` 17 | PreviousLoginTime int64 `json:"previous_logon_time"` 18 | Name string `json:"name"` 19 | } 20 | 21 | // GetMe retrieves the UserInfo for the current user. 22 | func (a *API) GetMe() (*UserInfo, error) { 23 | u := urlWithPath(*a.TargetURL, "/userinfo") 24 | u.RawQuery = "scheme=openid" 25 | 26 | info := &UserInfo{} 27 | err := a.doJSON(http.MethodGet, &u, nil, info, true) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return info, nil 32 | } 33 | -------------------------------------------------------------------------------- /me_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | uaa "github.com/cloudfoundry-community/go-uaa" 9 | . "github.com/onsi/gomega" 10 | "github.com/sclevine/spec" 11 | ) 12 | 13 | func testMe(t *testing.T, when spec.G, it spec.S) { 14 | var ( 15 | s *httptest.Server 16 | handler http.Handler 17 | called int 18 | a *uaa.API 19 | userinfoJSON string 20 | ) 21 | 22 | it.Before(func() { 23 | RegisterTestingT(t) 24 | called = 0 25 | userinfoJSON = `{ 26 | "user_id": "d6ef6c2e-02f6-477a-a7c6-18e27f9a6e87", 27 | "sub": "d6ef6c2e-02f6-477a-a7c6-18e27f9a6e87", 28 | "user_name": "charlieb", 29 | "given_name": "Charlie", 30 | "family_name": "Brown", 31 | "email": "charlieb@peanuts.com", 32 | "phone_number": null, 33 | "previous_logon_time": 1503123277743, 34 | "name": "Charlie Brown" 35 | }` 36 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 37 | called = called + 1 38 | Expect(handler).NotTo(BeNil()) 39 | handler.ServeHTTP(w, req) 40 | })) 41 | a, _ = uaa.New(s.URL, uaa.WithNoAuthentication()) 42 | }) 43 | 44 | it.After(func() { 45 | if s != nil { 46 | s.Close() 47 | } 48 | }) 49 | 50 | it("calls the /userinfo endpoint", func() { 51 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 52 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 53 | Expect(req.URL.Path).To(Equal("/userinfo")) 54 | Expect(req.URL.Query().Get("scheme")).To(Equal("openid")) 55 | w.WriteHeader(http.StatusOK) 56 | _, err := w.Write([]byte(userinfoJSON)) 57 | Expect(err).NotTo(HaveOccurred()) 58 | }) 59 | 60 | userinfo, err := a.GetMe() 61 | Expect(err).NotTo(HaveOccurred()) 62 | Expect(userinfo).NotTo(BeNil()) 63 | Expect(called).To(Equal(1)) 64 | Expect(userinfo.UserID).To(Equal("d6ef6c2e-02f6-477a-a7c6-18e27f9a6e87")) 65 | Expect(userinfo.Sub).To(Equal("d6ef6c2e-02f6-477a-a7c6-18e27f9a6e87")) 66 | Expect(userinfo.Username).To(Equal("charlieb")) 67 | Expect(userinfo.GivenName).To(Equal("Charlie")) 68 | Expect(userinfo.FamilyName).To(Equal("Brown")) 69 | Expect(userinfo.Email).To(Equal("charlieb@peanuts.com")) 70 | }) 71 | 72 | it("returns helpful error when /userinfo request fails", func() { 73 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 74 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 75 | Expect(req.URL.Path).To(Equal("/userinfo")) 76 | Expect(req.URL.Query().Get("scheme")).To(Equal("openid")) 77 | w.WriteHeader(http.StatusInternalServerError) 78 | }) 79 | u, err := a.GetMe() 80 | Expect(err).To(HaveOccurred()) 81 | Expect(u).To(BeNil()) 82 | Expect(err.Error()).To(ContainSubstring("An error occurred while calling")) 83 | }) 84 | 85 | it("returns helpful error when /userinfo response can't be parsed", func() { 86 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 87 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 88 | Expect(req.URL.Path).To(Equal("/userinfo")) 89 | Expect(req.URL.Query().Get("scheme")).To(Equal("openid")) 90 | w.WriteHeader(http.StatusOK) 91 | _, err := w.Write([]byte("{unparsable-json-response}")) 92 | Expect(err).NotTo(HaveOccurred()) 93 | }) 94 | u, err := a.GetMe() 95 | Expect(err).To(HaveOccurred()) 96 | Expect(u).To(BeNil()) 97 | Expect(err.Error()).To(ContainSubstring("An unknown error occurred while parsing response from")) 98 | Expect(err.Error()).To(ContainSubstring("Response was {unparsable-json-response}")) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /mfa_provider.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | // MFAProvidersEndpoint is the path to the MFA providers resource. 4 | const MFAProvidersEndpoint string = "/mfa-providers" 5 | 6 | // MFAProviderConfig is configuration for an MFA provider 7 | type MFAProviderConfig struct { 8 | Issuer string `json:"issuer,omitempty"` 9 | ProviderDescription string `json:"providerDescription,omitempty"` 10 | } 11 | 12 | // MFAProvider is a UAA MFA provider 13 | // http://docs.cloudfoundry.org/api/uaa/version/4.19.0/index.html#get-2 14 | type MFAProvider struct { 15 | ID string `json:"id,omitempty"` 16 | Name string `json:"name"` 17 | IdentityZoneID string `json:"identityZoneId,omitempty"` 18 | Config MFAProviderConfig `json:"config"` 19 | Type string `json:"type"` 20 | Created int `json:"created,omitempty"` 21 | LastModified int `json:"last_modified,omitempty"` 22 | } 23 | 24 | // Identifier returns the field used to uniquely identify a MFAProvider. 25 | func (m MFAProvider) Identifier() string { 26 | return m.ID 27 | } 28 | -------------------------------------------------------------------------------- /mfa_provider_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import uaa "github.com/cloudfoundry-community/go-uaa" 4 | 5 | var mfaproviderResponse string = `{ 6 | "id": "00000000-0000-0000-0000-000000000001", 7 | "name": "sampleGoogleMfaProvider8ZFKcx", 8 | "identityZoneId": "uaa", 9 | "config": { 10 | "issuer": "uaa", 11 | "providerDescription": "Google MFA for default zone" 12 | }, 13 | "type": "google-authenticator", 14 | "created": 1529690500934, 15 | "last_modified": 1529690500934 16 | }` 17 | 18 | var mfaproviderListResponse string = `[{ 19 | "id": "00000000-0000-0000-0000-000000000001", 20 | "name": "sampleGoogleMfaProviderCJTjGb", 21 | "identityZoneId": "uaa", 22 | "config" : { 23 | "issuer": "uaa", 24 | "providerDescription": "Google MFA for default zone" 25 | }, 26 | "type": "google-authenticator", 27 | "created": 1529690500558, 28 | "last_modified": 1529690500558 29 | }, { 30 | "id": "00000000-0000-0000-0000-000000000002", 31 | "name": "sampleGoogleMfaProviderUKaW73", 32 | "identityZoneId": "uaa", 33 | "config": { 34 | "issuer": "uaa", 35 | "providerDescription": "Google MFA for default zone" 36 | }, 37 | "type": "google-authenticator", 38 | "created" : 1529690500430, 39 | "last_modified" : 1529690500430 40 | }]` 41 | 42 | var testMFAProviderValue uaa.MFAProvider = uaa.MFAProvider{ 43 | ID: "00000000-0000-0000-0000-000000000001", 44 | Name: "sampleGoogleMfaProvider8ZFKcx", 45 | IdentityZoneID: "uaa", 46 | Config: uaa.MFAProviderConfig{ 47 | Issuer: "uaa", 48 | ProviderDescription: "Google MFA for default zone", 49 | }, 50 | Type: "google-authenticator", 51 | Created: 1529690500934, 52 | LastModified: 1529690500934, 53 | } 54 | 55 | var testMFAProviderJSON string = `{ 56 | "id": "00000000-0000-0000-0000-000000000001", 57 | "name": "sampleGoogleMfaProvider8ZFKcx", 58 | "identityZoneId": "uaa", 59 | "config": { 60 | "issuer": "uaa", 61 | "providerDescription": "Google MFA for default zone" 62 | }, 63 | "type": "google-authenticator", 64 | "created": 1529690500934, 65 | "last_modified": 1529690500934 66 | }` 67 | -------------------------------------------------------------------------------- /page.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | // Page represents a page of information returned from the UAA API. 4 | type Page struct { 5 | StartIndex int `json:"startIndex"` 6 | ItemsPerPage int `json:"itemsPerPage"` 7 | TotalResults int `json:"totalResults"` 8 | } 9 | -------------------------------------------------------------------------------- /passwordcredentials/README.md: -------------------------------------------------------------------------------- 1 | ### Password Credentials Token Source 2 | 3 | This package is extracted from https://github.com/golang/oauth2/issues/186. When 4 | the `passwordcredentials` token source is included in the standard library, this 5 | package will be removed and the go-uaa package will switch to use the standard 6 | library implementation. 7 | 8 | ### License 9 | 10 | > Copyright 2014 The Go Authors. All rights reserved. 11 | > Use of this source code is governed by a BSD-style 12 | > license that can be found in the LICENSE file: 13 | > 14 | > https://github.com/golang/oauth2/blob/1e0a3fa8ba9a5c9eb35c271780101fdaf1b205d7/LICENSE 15 | -------------------------------------------------------------------------------- /passwordcredentials/passwordcredentials.go: -------------------------------------------------------------------------------- 1 | // Package passwordcredentials implements the OAuth2.0 "password credentials" token flow. 2 | // See https://tools.ietf.org/html/rfc6749#section-4.3 3 | package passwordcredentials 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "mime" 11 | "net/http" 12 | "net/url" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "context" 18 | 19 | "golang.org/x/oauth2" 20 | ) 21 | 22 | func retrieveToken(ctx context.Context, ClientID, ClientSecret, TokenURL string, v url.Values) (*oauth2.Token, error) { 23 | hc := ContextClient(ctx) 24 | v.Set("client_id", ClientID) 25 | req, err := http.NewRequest("POST", TokenURL, strings.NewReader(v.Encode())) 26 | if err != nil { 27 | return nil, err 28 | } 29 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 30 | req.SetBasicAuth(ClientID, ClientSecret) 31 | r, err := hc.Do(req) 32 | if err != nil { 33 | return nil, err 34 | } 35 | body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) 36 | r.Body.Close() 37 | if err != nil { 38 | return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 39 | } 40 | if code := r.StatusCode; code < 200 || code > 299 { 41 | return nil, &oauth2.RetrieveError{ 42 | Response: r, 43 | Body: body, 44 | } 45 | } 46 | 47 | var token *internalToken 48 | content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) 49 | switch content { 50 | case "application/x-www-form-urlencoded", "text/plain": 51 | vals, err := url.ParseQuery(string(body)) 52 | if err != nil { 53 | return nil, err 54 | } 55 | token = &internalToken{ 56 | AccessToken: vals.Get("access_token"), 57 | TokenType: vals.Get("token_type"), 58 | RefreshToken: vals.Get("refresh_token"), 59 | Raw: vals, 60 | } 61 | e := vals.Get("expires_in") 62 | if e == "" { 63 | // TODO(jbd): Facebook's OAuth2 implementation is broken and 64 | // returns expires_in field in expires. Remove the fallback to expires, 65 | // when Facebook fixes their implementation. 66 | e = vals.Get("expires") 67 | } 68 | expires, _ := strconv.Atoi(e) 69 | if expires != 0 { 70 | token.Expiry = time.Now().Add(time.Duration(expires) * time.Second) 71 | } 72 | default: 73 | var tj tokenJSON 74 | if err = json.Unmarshal(body, &tj); err != nil { 75 | return nil, err 76 | } 77 | token = &internalToken{ 78 | AccessToken: tj.AccessToken, 79 | TokenType: tj.TokenType, 80 | RefreshToken: tj.RefreshToken, 81 | Expiry: tj.expiry(), 82 | Raw: make(map[string]interface{}), 83 | } 84 | err = json.Unmarshal(body, &token.Raw) // no error checks for optional fields 85 | if err != nil { 86 | return nil, err 87 | } 88 | } 89 | // Don't overwrite `RefreshToken` with an empty value 90 | // if this was a token refreshing request. 91 | if token.RefreshToken == "" { 92 | token.RefreshToken = v.Get("refresh_token") 93 | } 94 | if token == nil { 95 | return nil, nil 96 | } 97 | tk := &oauth2.Token{ 98 | AccessToken: token.AccessToken, 99 | TokenType: token.TokenType, 100 | RefreshToken: token.RefreshToken, 101 | Expiry: token.Expiry, 102 | } 103 | return tk.WithExtra(token.Raw), nil 104 | } 105 | 106 | func ContextClient(ctx context.Context) *http.Client { 107 | if ctx != nil { 108 | if hc, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok { 109 | return hc 110 | } 111 | } 112 | return http.DefaultClient 113 | } 114 | 115 | // Token represents the crendentials used to authorize 116 | // the requests to access protected resources on the OAuth 2.0 117 | // provider's backend. 118 | // 119 | // This type is a mirror of oauth2.Token and exists to break 120 | // an otherwise-circular dependency. Other internal packages 121 | // should convert this Token into an oauth2.Token before use. 122 | type internalToken struct { 123 | // AccessToken is the token that authorizes and authenticates 124 | // the requests. 125 | AccessToken string 126 | 127 | // TokenType is the type of token. 128 | // The Type method returns either this or "Bearer", the default. 129 | TokenType string 130 | 131 | // RefreshToken is a token that's used by the application 132 | // (as opposed to the user) to refresh the access token 133 | // if it expires. 134 | RefreshToken string 135 | 136 | // Expiry is the optional expiration time of the access token. 137 | // 138 | // If zero, TokenSource implementations will reuse the same 139 | // token forever and RefreshToken or equivalent 140 | // mechanisms for that TokenSource will not be used. 141 | Expiry time.Time 142 | 143 | // Raw optionally contains extra metadata from the server 144 | // when updating a token. 145 | Raw interface{} 146 | } 147 | 148 | // Config describes a Resource Owner Password Credentials OAuth2 flow, with the 149 | // client application information, resource owner credentials and the server's 150 | // endpoint URLs. 151 | type Config struct { 152 | // ClientID is the application's ID. 153 | ClientID string 154 | 155 | // ClientSecret is the application's secret. 156 | ClientSecret string 157 | 158 | // Resource owner username 159 | Username string 160 | 161 | // Resource owner password 162 | Password string 163 | 164 | // Endpoint contains the resource server's token endpoint 165 | // URLs. These are constants specific to each server and are 166 | // often available via site-specific packages, such as 167 | // google.Endpoint or github.Endpoint. 168 | Endpoint oauth2.Endpoint 169 | 170 | // Scope specifies optional requested permissions. 171 | Scopes []string 172 | 173 | // EndpointParams specifies additional parameters for requests to the token endpoint. 174 | EndpointParams url.Values 175 | } 176 | 177 | // tokenJSON is the struct representing the HTTP response from OAuth2 178 | // providers returning a token in JSON form. 179 | type tokenJSON struct { 180 | AccessToken string `json:"access_token"` 181 | TokenType string `json:"token_type"` 182 | RefreshToken string `json:"refresh_token"` 183 | ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number 184 | Expires expirationTime `json:"expires"` // broken Facebook spelling of expires_in 185 | } 186 | 187 | func (e *tokenJSON) expiry() (t time.Time) { 188 | if v := e.ExpiresIn; v != 0 { 189 | return time.Now().Add(time.Duration(v) * time.Second) 190 | } 191 | if v := e.Expires; v != 0 { 192 | return time.Now().Add(time.Duration(v) * time.Second) 193 | } 194 | return 195 | } 196 | 197 | type expirationTime int32 198 | 199 | func (e *expirationTime) UnmarshalJSON(b []byte) error { 200 | var n json.Number 201 | err := json.Unmarshal(b, &n) 202 | if err != nil { 203 | return err 204 | } 205 | i, err := n.Int64() 206 | if err != nil { 207 | return err 208 | } 209 | *e = expirationTime(i) 210 | return nil 211 | } 212 | 213 | // Client returns an HTTP client using the provided token. 214 | // The token will auto-refresh as necessary. The underlying 215 | // HTTP transport will be obtained using the provided context. 216 | // The returned client and its Transport should not be modified. 217 | func (c *Config) Client(ctx context.Context) *http.Client { 218 | return oauth2.NewClient(ctx, c.TokenSource(ctx)) 219 | } 220 | 221 | // TokenSource returns a TokenSource that returns t until t expires, 222 | // automatically refreshing it as necessary using the provided context and the 223 | // client ID and client secret. 224 | // 225 | // Most users will use Config.Client instead. 226 | func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { 227 | source := &tokenSource{ 228 | ctx: ctx, 229 | conf: c, 230 | } 231 | return oauth2.ReuseTokenSource(nil, source) 232 | } 233 | 234 | type tokenSource struct { 235 | ctx context.Context 236 | conf *Config 237 | } 238 | 239 | // Token refreshes the token by using a new password credentials request. 240 | // tokens received this way do not include a refresh token 241 | func (c *tokenSource) Token() (*oauth2.Token, error) { 242 | v := url.Values{ 243 | "grant_type": {"password"}, 244 | "username": {c.conf.Username}, 245 | "password": {c.conf.Password}, 246 | } 247 | if len(c.conf.Scopes) > 0 { 248 | v.Set("scope", strings.Join(c.conf.Scopes, " ")) 249 | } 250 | for k, p := range c.conf.EndpointParams { 251 | if _, ok := v[k]; ok { 252 | return nil, fmt.Errorf("oauth2: cannot overwrite parameter %q", k) 253 | } 254 | v[k] = p 255 | } 256 | return retrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.Endpoint.TokenURL, v) 257 | } 258 | -------------------------------------------------------------------------------- /passwordcredentials/passwordcredentials_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package passwordcredentials 6 | 7 | import ( 8 | "context" 9 | "io/ioutil" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | 14 | "golang.org/x/oauth2" 15 | ) 16 | 17 | func newConf(url string) *Config { 18 | return &Config{ 19 | ClientID: "CLIENT_ID", 20 | ClientSecret: "CLIENT_SECRET", 21 | Username: "USERNAME", 22 | Password: "PASSWORD", 23 | Scopes: []string{"scope1", "scope2"}, 24 | Endpoint: oauth2.Endpoint{ 25 | TokenURL: url + "/token", 26 | }, 27 | } 28 | } 29 | 30 | func TestTokenRequest(t *testing.T) { 31 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 | if r.URL.String() != "/token" { 33 | t.Errorf("authenticate client request URL = %q; want %q", r.URL, "/token") 34 | } 35 | headerAuth := r.Header.Get("Authorization") 36 | if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" { 37 | t.Errorf("Unexpected authorization header, %v is found.", headerAuth) 38 | } 39 | if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want { 40 | t.Errorf("Content-Type header = %q; want %q", got, want) 41 | } 42 | body, err := ioutil.ReadAll(r.Body) 43 | if err != nil { 44 | r.Body.Close() 45 | } 46 | if err != nil { 47 | t.Errorf("failed reading request body: %s.", err) 48 | } 49 | want := "client_id=CLIENT_ID&grant_type=password&password=PASSWORD&scope=scope1+scope2&username=USERNAME" 50 | if string(body) != want { 51 | t.Errorf("payload = %q; want %q", string(body), want) 52 | } 53 | w.Header().Set("Content-Type", "application/x-www-form-urlencoded") 54 | _, err = w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&token_type=bearer")) 55 | if err != nil { 56 | t.Errorf("failed writing response: %s.", err) 57 | } 58 | })) 59 | defer ts.Close() 60 | conf := newConf(ts.URL) 61 | tok, err := conf.TokenSource(context.Background()).Token() 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | if !tok.Valid() { 66 | t.Fatalf("token invalid. got: %#v", tok) 67 | } 68 | if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" { 69 | t.Errorf("Access token = %q; want %q", tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c") 70 | } 71 | if tok.TokenType != "bearer" { 72 | t.Errorf("token type = %q; want %q", tok.TokenType, "bearer") 73 | } 74 | } 75 | 76 | func TestTokenRefreshRequest(t *testing.T) { 77 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 78 | if r.URL.String() == "/somethingelse" { 79 | return 80 | } 81 | if r.URL.String() != "/token" { 82 | t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL) 83 | } 84 | headerContentType := r.Header.Get("Content-Type") 85 | if headerContentType != "application/x-www-form-urlencoded" { 86 | t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType) 87 | } 88 | body, _ := ioutil.ReadAll(r.Body) 89 | want := "client_id=CLIENT_ID&grant_type=password&password=PASSWORD&scope=scope1+scope2&username=USERNAME" 90 | if string(body) != want { 91 | t.Errorf("payload = %q; want %q", string(body), want) 92 | } 93 | })) 94 | defer ts.Close() 95 | conf := newConf(ts.URL) 96 | c := conf.Client(context.Background()) 97 | _, _ = c.Get(ts.URL + "/somethingelse") 98 | } 99 | -------------------------------------------------------------------------------- /request_errors.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | "golang.org/x/oauth2" 8 | ) 9 | 10 | type RequestError struct { 11 | Url string 12 | ErrorResponse []byte 13 | } 14 | 15 | func (r RequestError) Error() string { 16 | return fmt.Sprintf("An error occurred while calling %s %s", r.Url, string(r.ErrorResponse)) 17 | } 18 | 19 | func requestErrorFromOauthError(err error) error { 20 | oauthErrorResponse, isRetrieveError := err.(*oauth2.RetrieveError) 21 | if isRetrieveError { 22 | tokenUrl := oauthErrorResponse.Response.Request.URL.String() 23 | return requestErrorWithBody(tokenUrl, oauthErrorResponse.Body) 24 | } 25 | return err 26 | } 27 | 28 | func requestErrorWithBody(url string, body []byte) error { 29 | return RequestError{url, body} 30 | } 31 | 32 | func requestError(url string) error { 33 | return errors.Errorf("An error occurred while calling %s", url) 34 | } 35 | 36 | func parseError(err error, url string, body []byte) error { 37 | return errors.Wrapf(err, "An unknown error occurred while parsing response from %s. Response was %s", url, string(body)) 38 | } 39 | -------------------------------------------------------------------------------- /roundtrip.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "time" 13 | 14 | "errors" 15 | 16 | "golang.org/x/oauth2" 17 | ) 18 | 19 | func (a *API) doJSON(method string, url *url.URL, body io.Reader, response interface{}, needsAuthentication bool) error { 20 | if strings.Contains(url.Path, "/Users/") || strings.Contains(url.Path, "/Groups/") && method == "PUT" { 21 | return a.doJSONWithHeaders(method, url, map[string]string{"If-Match": "*"}, body, response, needsAuthentication) 22 | } 23 | return a.doJSONWithHeaders(method, url, nil, body, response, needsAuthentication) 24 | } 25 | 26 | func (a *API) doJSONWithHeaders(method string, url *url.URL, headers map[string]string, body io.Reader, response interface{}, needsAuthentication bool) error { 27 | req, err := http.NewRequest(method, url.String(), body) 28 | if err != nil { 29 | return err 30 | } 31 | for k, v := range headers { 32 | req.Header.Set(k, v) 33 | } 34 | 35 | bytes, err := a.doAndRead(req, needsAuthentication) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | if response != nil { 41 | if err := json.Unmarshal(bytes, response); err != nil { 42 | return parseError(err, url.String(), bytes) 43 | } 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func (a *API) doAndRead(req *http.Request, needsAuthentication bool) ([]byte, error) { 50 | req.Header.Add("Accept", "application/json") 51 | req.Header.Add("X-Identity-Zone-Id", a.zoneID) 52 | userAgent := a.userAgent 53 | if userAgent == "" { 54 | userAgent = "go-uaa" 55 | } 56 | req.Header.Set("User-Agent", userAgent) 57 | switch req.Method { 58 | case http.MethodPut, http.MethodPost, http.MethodPatch: 59 | req.Header.Add("Content-Type", "application/json") 60 | } 61 | a.ensureTimeout() 62 | var ( 63 | resp *http.Response 64 | err error 65 | ) 66 | if !needsAuthentication && a.baseClient != nil { 67 | a.ensureTransport(a.baseClient.Transport) 68 | resp, err = a.baseClient.Do(req) 69 | } else { 70 | if a.Client == nil { 71 | return nil, errors.New("doAndRead: the Client cannot be nil") 72 | } 73 | a.ensureTransport(a.Client.Transport) 74 | resp, err = a.Client.Do(req) 75 | } 76 | 77 | if err != nil { 78 | if a.verbose { 79 | fmt.Printf("%v\n\n", err) 80 | } 81 | 82 | return nil, requestError(req.URL.String()) 83 | } 84 | 85 | bytes, err := ioutil.ReadAll(resp.Body) 86 | if err != nil { 87 | if a.verbose { 88 | fmt.Printf("%v\n\n", err) 89 | } 90 | return nil, requestError(req.URL.String()) 91 | } 92 | 93 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 94 | if len(bytes) > 0 { 95 | return nil, requestErrorWithBody(req.URL.String(), bytes) 96 | } 97 | return nil, requestError(req.URL.String()) 98 | } 99 | return bytes, nil 100 | } 101 | 102 | func (a *API) ensureTimeout() { 103 | if a.Client != nil && a.Client.Timeout == 0 { 104 | a.Client.Timeout = time.Second * 120 105 | } 106 | 107 | if a.baseClient != nil && a.baseClient.Timeout == 0 { 108 | a.baseClient.Timeout = time.Second * 120 109 | } 110 | } 111 | 112 | func (a *API) ensureTransport(c http.RoundTripper) { 113 | if c == nil { 114 | return 115 | } 116 | switch t := c.(type) { 117 | case *oauth2.Transport: 118 | b, ok := t.Base.(*http.Transport) 119 | if !ok { 120 | return 121 | } 122 | if b.TLSClientConfig == nil && !a.skipSSLValidation { 123 | return 124 | } 125 | if b.TLSClientConfig == nil { 126 | b.TLSClientConfig = &tls.Config{} 127 | } 128 | b.TLSClientConfig.InsecureSkipVerify = a.skipSSLValidation 129 | case *tokenTransport: 130 | a.ensureTransport(t.underlyingTransport) 131 | case *http.Transport: 132 | if t.TLSClientConfig == nil && !a.skipSSLValidation { 133 | return 134 | } 135 | if t.TLSClientConfig == nil { 136 | t.TLSClientConfig = &tls.Config{} 137 | } 138 | t.TLSClientConfig.InsecureSkipVerify = a.skipSSLValidation 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /roundtrip_test.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "net/http/httptest" 10 | 11 | . "github.com/onsi/gomega" 12 | "github.com/sclevine/spec" 13 | "golang.org/x/oauth2" 14 | ) 15 | 16 | func testEnsureTransport(t *testing.T, when spec.G, it spec.S) { 17 | var a *API 18 | it.Before(func() { 19 | RegisterTestingT(t) 20 | a = &API{} 21 | }) 22 | 23 | when("the transport is nil", func() { 24 | it("is a no-op", func() { 25 | do := func() { 26 | a.ensureTransport(nil) 27 | } 28 | Expect(do).NotTo(Panic()) 29 | }) 30 | }) 31 | 32 | when("the client is not set but the base client is set", func() { 33 | var s *httptest.Server 34 | 35 | it.Before(func() { 36 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {})) 37 | a.Client = &http.Client{} 38 | a.baseClient = &http.Client{} 39 | }) 40 | 41 | it.After(func() { 42 | if s != nil { 43 | s.Close() 44 | } 45 | }) 46 | 47 | it("will make a http call with the unauthenticated client", func() { 48 | req, err := http.NewRequest("GET", s.URL, nil) 49 | Expect(err).NotTo(HaveOccurred()) 50 | _, err = a.doAndRead(req, false) 51 | Expect(err).NotTo(HaveOccurred()) 52 | }) 53 | }) 54 | 55 | when("the client is set but the base client is not set", func() { 56 | var s *httptest.Server 57 | 58 | it.Before(func() { 59 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {})) 60 | a.Client = &http.Client{} 61 | }) 62 | 63 | it.After(func() { 64 | if s != nil { 65 | s.Close() 66 | } 67 | }) 68 | 69 | it("will make an http call with the client, even when needsAuthentication is false", func() { 70 | req, err := http.NewRequest("GET", s.URL, nil) 71 | Expect(err).NotTo(HaveOccurred()) 72 | _, err = a.doAndRead(req, false) 73 | Expect(err).NotTo(HaveOccurred()) 74 | }) 75 | }) 76 | 77 | when("the client transport is not set", func() { 78 | it.Before(func() { 79 | a.baseClient = &http.Client{} 80 | }) 81 | 82 | it("is a no-op", func() { 83 | a.ensureTransport(a.baseClient.Transport) 84 | Expect(a.baseClient).NotTo(BeNil()) 85 | Expect(a.baseClient.Transport).To(BeNil()) 86 | }) 87 | }) 88 | 89 | when("the client transport is an http.Transport", func() { 90 | it.Before(func() { 91 | a.baseClient = &http.Client{Transport: &http.Transport{}} 92 | }) 93 | 94 | when("skipSSLValidation is false", func() { 95 | it.Before(func() { 96 | a.skipSSLValidation = false 97 | }) 98 | 99 | it("will not initialize the TLS client config", func() { 100 | a.ensureTransport(a.baseClient.Transport) 101 | Expect(a.baseClient).NotTo(BeNil()) 102 | Expect(a.baseClient.Transport).NotTo(BeNil()) 103 | t := a.baseClient.Transport.(*http.Transport) 104 | Expect(t.TLSClientConfig).To(BeNil()) 105 | }) 106 | }) 107 | 108 | when("skipSSLValidation is true", func() { 109 | it.Before(func() { 110 | a.skipSSLValidation = true 111 | }) 112 | 113 | it("will initialize the TLS client config and set InsecureSkipVerify", func() { 114 | a.ensureTransport(a.baseClient.Transport) 115 | Expect(a.baseClient).NotTo(BeNil()) 116 | Expect(a.baseClient.Transport).NotTo(BeNil()) 117 | t := a.baseClient.Transport.(*http.Transport) 118 | Expect(t.TLSClientConfig).NotTo(BeNil()) 119 | Expect(t.TLSClientConfig.InsecureSkipVerify).To(BeTrue()) 120 | }) 121 | }) 122 | }) 123 | 124 | when("the client transport is a tokenTransport", func() { 125 | it.Before(func() { 126 | a.baseClient = &http.Client{Transport: &tokenTransport{ 127 | underlyingTransport: &http.Transport{ 128 | Proxy: http.ProxyFromEnvironment, 129 | DialContext: (&net.Dialer{ 130 | Timeout: 30 * time.Second, 131 | KeepAlive: 30 * time.Second, 132 | DualStack: true, 133 | }).DialContext, 134 | MaxIdleConns: 100, 135 | IdleConnTimeout: 90 * time.Second, 136 | TLSHandshakeTimeout: 10 * time.Second, 137 | ExpectContinueTimeout: 1 * time.Second, 138 | }, 139 | }} 140 | }) 141 | 142 | when("skipSSLValidation is false", func() { 143 | it.Before(func() { 144 | a.skipSSLValidation = false 145 | }) 146 | 147 | it("will not initialize the TLS client config", func() { 148 | a.ensureTransport(a.baseClient.Transport) 149 | Expect(a.baseClient).NotTo(BeNil()) 150 | Expect(a.baseClient.Transport).NotTo(BeNil()) 151 | t := a.baseClient.Transport.(*tokenTransport) 152 | c := t.underlyingTransport.(*http.Transport) 153 | Expect(c.TLSClientConfig).To(BeNil()) 154 | }) 155 | }) 156 | 157 | when("skipSSLValidation is true", func() { 158 | it.Before(func() { 159 | a.skipSSLValidation = true 160 | }) 161 | 162 | it("will initialize the TLS client config and set InsecureSkipVerify", func() { 163 | a.ensureTransport(a.baseClient.Transport) 164 | Expect(a.baseClient).NotTo(BeNil()) 165 | Expect(a.baseClient.Transport).NotTo(BeNil()) 166 | t := a.baseClient.Transport.(*tokenTransport) 167 | c := t.underlyingTransport.(*http.Transport) 168 | Expect(c.TLSClientConfig).NotTo(BeNil()) 169 | Expect(c.TLSClientConfig.InsecureSkipVerify).To(BeTrue()) 170 | }) 171 | }) 172 | }) 173 | 174 | when("the client transport is an oauth2.Transport but the Base transport is nil", func() { 175 | it.Before(func() { 176 | a.baseClient = &http.Client{Transport: &oauth2.Transport{}} 177 | }) 178 | 179 | it("is a no-op", func() { 180 | a.ensureTransport(a.baseClient.Transport) 181 | Expect(a.baseClient).NotTo(BeNil()) 182 | Expect(a.baseClient.Transport).NotTo(BeNil()) 183 | t := a.baseClient.Transport.(*oauth2.Transport) 184 | Expect(t.Base).To(BeNil()) 185 | }) 186 | }) 187 | 188 | when("the client transport is an oauth2.Transport with a Base transport", func() { 189 | it.Before(func() { 190 | a.baseClient = &http.Client{Transport: &oauth2.Transport{ 191 | Base: &http.Transport{}, 192 | }} 193 | }) 194 | 195 | when("skipSSLValidation is false", func() { 196 | it.Before(func() { 197 | a.skipSSLValidation = false 198 | }) 199 | 200 | it("will not initialize the TLS client config if skipSSLValidation is false", func() { 201 | a.ensureTransport(a.baseClient.Transport) 202 | Expect(a.baseClient).NotTo(BeNil()) 203 | Expect(a.baseClient.Transport).NotTo(BeNil()) 204 | t := a.baseClient.Transport.(*oauth2.Transport) 205 | Expect(t.Base).NotTo(BeNil()) 206 | b := t.Base.(*http.Transport) 207 | Expect(b.TLSClientConfig).To(BeNil()) 208 | }) 209 | }) 210 | 211 | when("skipSSLValidation is true", func() { 212 | it.Before(func() { 213 | a.skipSSLValidation = true 214 | }) 215 | 216 | it("will initialize the TLS client config and set InsecureSkipVerify", func() { 217 | a.ensureTransport(a.baseClient.Transport) 218 | Expect(a.baseClient).NotTo(BeNil()) 219 | Expect(a.baseClient.Transport).NotTo(BeNil()) 220 | t := a.baseClient.Transport.(*oauth2.Transport) 221 | Expect(t.Base).NotTo(BeNil()) 222 | b := t.Base.(*http.Transport) 223 | Expect(b.TLSClientConfig).NotTo(BeNil()) 224 | Expect(b.TLSClientConfig.InsecureSkipVerify).To(BeTrue()) 225 | }) 226 | }) 227 | }) 228 | } 229 | -------------------------------------------------------------------------------- /sort.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | // SortOrder defines the sort order when listing users or groups. 4 | type SortOrder string 5 | 6 | const ( 7 | // SortAscending sorts in ascending order. 8 | SortAscending = SortOrder("ascending") 9 | // SortDescending sorts in descending order. 10 | SortDescending = SortOrder("descending") 11 | ) 12 | -------------------------------------------------------------------------------- /token_key.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // JWK represents a JSON Web Key (https://tools.ietf.org/html/rfc7517). 8 | type JWK struct { 9 | Kty string `json:"kty"` 10 | E string `json:"e,omitempty"` 11 | Use string `json:"use"` 12 | Kid string `json:"kid"` 13 | Alg string `json:"alg"` 14 | Value string `json:"value"` 15 | N string `json:"n,omitempty"` 16 | } 17 | 18 | // TokenKey retrieves a JWK from the token_key endpoint 19 | // (http://docs.cloudfoundry.org/api/uaa/version/4.14.0/index.html#token-key-s). 20 | func (a *API) TokenKey() (*JWK, error) { 21 | url := urlWithPath(*a.TargetURL, "/token_key") 22 | 23 | key := &JWK{} 24 | err := a.doJSON(http.MethodGet, &url, nil, key, false) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return key, err 29 | } 30 | -------------------------------------------------------------------------------- /token_key_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | uaa "github.com/cloudfoundry-community/go-uaa" 9 | . "github.com/onsi/gomega" 10 | "github.com/sclevine/spec" 11 | ) 12 | 13 | func testTokenKey(t *testing.T, when spec.G, it spec.S) { 14 | var ( 15 | s *httptest.Server 16 | handler http.Handler 17 | called int 18 | a *uaa.API 19 | asymmetricKeyJSON string 20 | ) 21 | 22 | it.Before(func() { 23 | RegisterTestingT(t) 24 | asymmetricKeyJSON = `{ 25 | "kty": "RSA", 26 | "e": "AQAB", 27 | "use": "sig", 28 | "kid": "sha2-2017-01-20-key", 29 | "alg": "RS256", 30 | "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyH6kYCP29faDAUPKtei3\nV/Zh8eCHyHRDHrD0iosvgHuaakK1AFHjD19ojuPiTQm8r8nEeQtHb6mDi1LvZ03e\nEWxpvWwFfFVtCyBqWr5wn6IkY+ZFXfERLn2NCn6sMVxcFV12sUtuqD+jrW8MnTG7\nhofQqxmVVKKsZiXCvUSzfiKxDgoiRuD3MJSoZ0nQTHVmYxlFHuhTEETuTqSPmOXd\n/xJBVRi5WYCjt1aKRRZEz04zVEBVhVkr2H84qcVJHcfXFu4JM6dg0nmTjgd5cZUN\ncwA1KhK2/Qru9N0xlk9FGD2cvrVCCPWFPvZ1W7U7PBWOSBBH6GergA+dk2vQr7Ho\nlQIDAQAB\n-----END PUBLIC KEY-----", 31 | "n": "AMh-pGAj9vX2gwFDyrXot1f2YfHgh8h0Qx6w9IqLL4B7mmpCtQBR4w9faI7j4k0JvK_JxHkLR2-pg4tS72dN3hFsab1sBXxVbQsgalq-cJ-iJGPmRV3xES59jQp-rDFcXBVddrFLbqg_o61vDJ0xu4aH0KsZlVSirGYlwr1Es34isQ4KIkbg9zCUqGdJ0Ex1ZmMZRR7oUxBE7k6kj5jl3f8SQVUYuVmAo7dWikUWRM9OM1RAVYVZK9h_OKnFSR3H1xbuCTOnYNJ5k44HeXGVDXMANSoStv0K7vTdMZZPRRg9nL61Qgj1hT72dVu1OzwVjkgQR-hnq4APnZNr0K-x6JU" 32 | }` 33 | called = 0 34 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 35 | called = called + 1 36 | Expect(handler).NotTo(BeNil()) 37 | handler.ServeHTTP(w, req) 38 | })) 39 | a, _ = uaa.New(s.URL, uaa.WithNoAuthentication()) 40 | }) 41 | 42 | it.After(func() { 43 | if s != nil { 44 | s.Close() 45 | } 46 | }) 47 | 48 | it("calls the /token_key endpoint", func() { 49 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 50 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 51 | Expect(req.URL.Path).To(Equal("/token_key")) 52 | w.WriteHeader(http.StatusOK) 53 | _, err := w.Write([]byte(asymmetricKeyJSON)) 54 | Expect(err).NotTo(HaveOccurred()) 55 | }) 56 | 57 | key, _ := a.TokenKey() 58 | 59 | Expect(called).To(Equal(1)) 60 | Expect(key.Kty).To(Equal("RSA")) 61 | Expect(key.E).To(Equal("AQAB")) 62 | Expect(key.Use).To(Equal("sig")) 63 | Expect(key.Kid).To(Equal("sha2-2017-01-20-key")) 64 | Expect(key.Alg).To(Equal("RS256")) 65 | Expect(key.Value).To(Equal("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyH6kYCP29faDAUPKtei3\nV/Zh8eCHyHRDHrD0iosvgHuaakK1AFHjD19ojuPiTQm8r8nEeQtHb6mDi1LvZ03e\nEWxpvWwFfFVtCyBqWr5wn6IkY+ZFXfERLn2NCn6sMVxcFV12sUtuqD+jrW8MnTG7\nhofQqxmVVKKsZiXCvUSzfiKxDgoiRuD3MJSoZ0nQTHVmYxlFHuhTEETuTqSPmOXd\n/xJBVRi5WYCjt1aKRRZEz04zVEBVhVkr2H84qcVJHcfXFu4JM6dg0nmTjgd5cZUN\ncwA1KhK2/Qru9N0xlk9FGD2cvrVCCPWFPvZ1W7U7PBWOSBBH6GergA+dk2vQr7Ho\nlQIDAQAB\n-----END PUBLIC KEY-----")) 66 | Expect(key.N).To(Equal("AMh-pGAj9vX2gwFDyrXot1f2YfHgh8h0Qx6w9IqLL4B7mmpCtQBR4w9faI7j4k0JvK_JxHkLR2-pg4tS72dN3hFsab1sBXxVbQsgalq-cJ-iJGPmRV3xES59jQp-rDFcXBVddrFLbqg_o61vDJ0xu4aH0KsZlVSirGYlwr1Es34isQ4KIkbg9zCUqGdJ0Ex1ZmMZRR7oUxBE7k6kj5jl3f8SQVUYuVmAo7dWikUWRM9OM1RAVYVZK9h_OKnFSR3H1xbuCTOnYNJ5k44HeXGVDXMANSoStv0K7vTdMZZPRRg9nL61Qgj1hT72dVu1OzwVjkgQR-hnq4APnZNr0K-x6JU")) 67 | }) 68 | 69 | it("returns helpful error when /token_key request fails", func() { 70 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 71 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 72 | Expect(req.URL.Path).To(Equal("/token_key")) 73 | w.WriteHeader(http.StatusInternalServerError) 74 | }) 75 | 76 | _, err := a.TokenKey() 77 | Expect(err).To(HaveOccurred()) 78 | Expect(err.Error()).To(ContainSubstring("An error occurred while calling")) 79 | }) 80 | 81 | it("returns helpful error when /token_key response can't be parsed", func() { 82 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 83 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 84 | Expect(req.URL.Path).To(Equal("/token_key")) 85 | w.WriteHeader(http.StatusOK) 86 | _, err := w.Write([]byte("{unparsable-json-response}")) 87 | Expect(err).NotTo(HaveOccurred()) 88 | }) 89 | _, err := a.TokenKey() 90 | Expect(err).To(HaveOccurred()) 91 | Expect(err.Error()).To(ContainSubstring("An unknown error occurred while parsing response from")) 92 | Expect(err.Error()).To(ContainSubstring("Response was {unparsable-json-response}")) 93 | }) 94 | 95 | it("can handle symmetric keys", func() { 96 | symmetricKeyJSON := `{ 97 | "kty" : "MAC", 98 | "alg" : "HS256", 99 | "value" : "key", 100 | "use" : "sig", 101 | "kid" : "testKey" 102 | }` 103 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 104 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 105 | Expect(req.URL.Path).To(Equal("/token_key")) 106 | w.WriteHeader(http.StatusOK) 107 | _, err := w.Write([]byte(symmetricKeyJSON)) 108 | Expect(err).NotTo(HaveOccurred()) 109 | }) 110 | key, _ := a.TokenKey() 111 | Expect(called).To(Equal(1)) 112 | Expect(key.Kty).To(Equal("MAC")) 113 | Expect(key.Alg).To(Equal("HS256")) 114 | Expect(key.Value).To(Equal("key")) 115 | Expect(key.Use).To(Equal("sig")) 116 | Expect(key.Kid).To(Equal("testKey")) 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /token_keys.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Keys is a slice of JSON Web Keys. 8 | type Keys struct { 9 | Keys []JWK `json:"keys"` 10 | } 11 | 12 | // TokenKeys gets the JSON Web Token signing keys for the UAA server. 13 | func (a *API) TokenKeys() ([]JWK, error) { 14 | url := urlWithPath(*a.TargetURL, "/token_keys") 15 | keys := &Keys{} 16 | err := a.doJSON(http.MethodGet, &url, nil, keys, false) 17 | if err != nil { 18 | key, e := a.TokenKey() 19 | if e != nil { 20 | return nil, e 21 | } 22 | return []JWK{*key}, nil 23 | } 24 | return keys.Keys, err 25 | } 26 | -------------------------------------------------------------------------------- /token_keys_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | uaa "github.com/cloudfoundry-community/go-uaa" 9 | . "github.com/onsi/gomega" 10 | "github.com/sclevine/spec" 11 | ) 12 | 13 | func testTokenKeys(t *testing.T, when spec.G, it spec.S) { 14 | var ( 15 | s *httptest.Server 16 | handler http.Handler 17 | called int 18 | a *uaa.API 19 | tokenKeysJSON string 20 | ) 21 | 22 | it.Before(func() { 23 | RegisterTestingT(t) 24 | tokenKeysJSON = `{ 25 | "keys": [ 26 | { 27 | "kty": "RSA", 28 | "e": "AQAB", 29 | "use": "sig", 30 | "kid": "sha2-2017-01-20-key", 31 | "alg": "RS256", 32 | "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyH6kYCP29faDAUPKtei3\nV/Zh8eCHyHRDHrD0iosvgHuaakK1AFHjD19ojuPiTQm8r8nEeQtHb6mDi1LvZ03e\nEWxpvWwFfFVtCyBqWr5wn6IkY+ZFXfERLn2NCn6sMVxcFV12sUtuqD+jrW8MnTG7\nhofQqxmVVKKsZiXCvUSzfiKxDgoiRuD3MJSoZ0nQTHVmYxlFHuhTEETuTqSPmOXd\n/xJBVRi5WYCjt1aKRRZEz04zVEBVhVkr2H84qcVJHcfXFu4JM6dg0nmTjgd5cZUN\ncwA1KhK2/Qru9N0xlk9FGD2cvrVCCPWFPvZ1W7U7PBWOSBBH6GergA+dk2vQr7Ho\nlQIDAQAB\n-----END PUBLIC KEY-----", 33 | "n": "AMh-pGAj9vX2gwFDyrXot1f2YfHgh8h0Qx6w9IqLL4B7mmpCtQBR4w9faI7j4k0JvK_JxHkLR2-pg4tS72dN3hFsab1sBXxVbQsgalq-cJ-iJGPmRV3xES59jQp-rDFcXBVddrFLbqg_o61vDJ0xu4aH0KsZlVSirGYlwr1Es34isQ4KIkbg9zCUqGdJ0Ex1ZmMZRR7oUxBE7k6kj5jl3f8SQVUYuVmAo7dWikUWRM9OM1RAVYVZK9h_OKnFSR3H1xbuCTOnYNJ5k44HeXGVDXMANSoStv0K7vTdMZZPRRg9nL61Qgj1hT72dVu1OzwVjkgQR-hnq4APnZNr0K-x6JU" 34 | }, 35 | { 36 | "kty": "RSA", 37 | "e": "AQAB", 38 | "use": "sig", 39 | "kid": "legacy-token-key", 40 | "alg": "RS256", 41 | "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/aXmEImpdwWHJlYc4G8\n3BgZVmyhCdy7SCL0kM7wV5xCvRKK0k4nKjH0QW2E+0GIKzIj4JQhYU+MeZHrArfC\nrfthIXcio/Ll6NvoTPY77XA7U6vBGCiLdGYSGrN8y064cF2uM8d3AEgTT0RzLK3E\n688Ltq38GxnoXOUuLZmXS2HeHNd2bW/k6Eyd9Z3ymmdpVZXMyLwepNxU38WQS2bJ\nPYXYvRkzoZ1ru/deExwbecI18NCeO/GKp3f8bwKuC2j3LKFJIAwW3zFoDrcAxpC/\nJDG2RSTj//CRvhtd7JkeQLVKGyIHNtACaPT3tFT6scvVXHGPB5fRTLB8Lr+mK4RI\nBwIDAQAB\n-----END PUBLIC KEY-----", 42 | "n": "APP2l5hCJqXcFhyZWHOBvNwYGVZsoQncu0gi9JDO8FecQr0SitJOJyox9EFthPtBiCsyI-CUIWFPjHmR6wK3wq37YSF3IqPy5ejb6Ez2O-1wO1OrwRgoi3RmEhqzfMtOuHBdrjPHdwBIE09EcyytxOvPC7at_BsZ6FzlLi2Zl0th3hzXdm1v5OhMnfWd8ppnaVWVzMi8HqTcVN_FkEtmyT2F2L0ZM6Gda7v3XhMcG3nCNfDQnjvxiqd3_G8Crgto9yyhSSAMFt8xaA63AMaQvyQxtkUk4__wkb4bXeyZHkC1ShsiBzbQAmj097RU-rHL1VxxjweX0UywfC6_piuESAc" 43 | } 44 | ] 45 | }` 46 | called = 0 47 | s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 48 | called = called + 1 49 | Expect(handler).NotTo(BeNil()) 50 | handler.ServeHTTP(w, req) 51 | })) 52 | a, _ = uaa.New(s.URL, uaa.WithNoAuthentication()) 53 | }) 54 | 55 | it.After(func() { 56 | if s != nil { 57 | s.Close() 58 | } 59 | }) 60 | 61 | it("calls the /token_keys endpoint", func() { 62 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 63 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 64 | Expect(req.URL.Path).To(Equal("/token_keys")) 65 | w.WriteHeader(http.StatusOK) 66 | _, err := w.Write([]byte(tokenKeysJSON)) 67 | Expect(err).NotTo(HaveOccurred()) 68 | }) 69 | keys, _ := a.TokenKeys() 70 | Expect(called).To(Equal(1)) 71 | Expect(keys[0].Kty).To(Equal("RSA")) 72 | Expect(keys[0].E).To(Equal("AQAB")) 73 | Expect(keys[0].Use).To(Equal("sig")) 74 | Expect(keys[0].Kid).To(Equal("sha2-2017-01-20-key")) 75 | Expect(keys[0].Alg).To(Equal("RS256")) 76 | Expect(keys[0].Value).To(Equal("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyH6kYCP29faDAUPKtei3\nV/Zh8eCHyHRDHrD0iosvgHuaakK1AFHjD19ojuPiTQm8r8nEeQtHb6mDi1LvZ03e\nEWxpvWwFfFVtCyBqWr5wn6IkY+ZFXfERLn2NCn6sMVxcFV12sUtuqD+jrW8MnTG7\nhofQqxmVVKKsZiXCvUSzfiKxDgoiRuD3MJSoZ0nQTHVmYxlFHuhTEETuTqSPmOXd\n/xJBVRi5WYCjt1aKRRZEz04zVEBVhVkr2H84qcVJHcfXFu4JM6dg0nmTjgd5cZUN\ncwA1KhK2/Qru9N0xlk9FGD2cvrVCCPWFPvZ1W7U7PBWOSBBH6GergA+dk2vQr7Ho\nlQIDAQAB\n-----END PUBLIC KEY-----")) 77 | Expect(keys[0].N).To(Equal("AMh-pGAj9vX2gwFDyrXot1f2YfHgh8h0Qx6w9IqLL4B7mmpCtQBR4w9faI7j4k0JvK_JxHkLR2-pg4tS72dN3hFsab1sBXxVbQsgalq-cJ-iJGPmRV3xES59jQp-rDFcXBVddrFLbqg_o61vDJ0xu4aH0KsZlVSirGYlwr1Es34isQ4KIkbg9zCUqGdJ0Ex1ZmMZRR7oUxBE7k6kj5jl3f8SQVUYuVmAo7dWikUWRM9OM1RAVYVZK9h_OKnFSR3H1xbuCTOnYNJ5k44HeXGVDXMANSoStv0K7vTdMZZPRRg9nL61Qgj1hT72dVu1OzwVjkgQR-hnq4APnZNr0K-x6JU")) 78 | Expect(keys[1].Kid).To(Equal("legacy-token-key")) 79 | }) 80 | 81 | it("returns a helpful error when response cannot be parsed", func() { 82 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 83 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 84 | w.WriteHeader(http.StatusOK) 85 | _, err := w.Write([]byte("{unparsable}")) 86 | Expect(err).NotTo(HaveOccurred()) 87 | }) 88 | _, err := a.TokenKeys() 89 | Expect(err).NotTo(BeNil()) 90 | Expect(err.Error()).To(ContainSubstring("An unknown error occurred while parsing response from")) 91 | }) 92 | 93 | when("the server is an older UAA that is missing the /token_keys endpoint", func() { 94 | var tokenKeyJSON string = `{ 95 | "kty": "RSA", 96 | "e": "AQAB", 97 | "use": "sig", 98 | "kid": "sha2-2017-01-20-key", 99 | "alg": "RS256", 100 | "value": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyH6kYCP29faDAUPKtei3\nV/Zh8eCHyHRDHrD0iosvgHuaakK1AFHjD19ojuPiTQm8r8nEeQtHb6mDi1LvZ03e\nEWxpvWwFfFVtCyBqWr5wn6IkY+ZFXfERLn2NCn6sMVxcFV12sUtuqD+jrW8MnTG7\nhofQqxmVVKKsZiXCvUSzfiKxDgoiRuD3MJSoZ0nQTHVmYxlFHuhTEETuTqSPmOXd\n/xJBVRi5WYCjt1aKRRZEz04zVEBVhVkr2H84qcVJHcfXFu4JM6dg0nmTjgd5cZUN\ncwA1KhK2/Qru9N0xlk9FGD2cvrVCCPWFPvZ1W7U7PBWOSBBH6GergA+dk2vQr7Ho\nlQIDAQAB\n-----END PUBLIC KEY-----", 101 | "n": "AMh-pGAj9vX2gwFDyrXot1f2YfHgh8h0Qx6w9IqLL4B7mmpCtQBR4w9faI7j4k0JvK_JxHkLR2-pg4tS72dN3hFsab1sBXxVbQsgalq-cJ-iJGPmRV3xES59jQp-rDFcXBVddrFLbqg_o61vDJ0xu4aH0KsZlVSirGYlwr1Es34isQ4KIkbg9zCUqGdJ0Ex1ZmMZRR7oUxBE7k6kj5jl3f8SQVUYuVmAo7dWikUWRM9OM1RAVYVZK9h_OKnFSR3H1xbuCTOnYNJ5k44HeXGVDXMANSoStv0K7vTdMZZPRRg9nL61Qgj1hT72dVu1OzwVjkgQR-hnq4APnZNr0K-x6JU" 102 | }` 103 | 104 | it("falls back to /token_key endpoint", func() { 105 | handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 106 | Expect(req.Header.Get("Accept")).To(Equal("application/json")) 107 | if req.URL.Path == "/token_keys" { 108 | w.WriteHeader(http.StatusNotFound) 109 | } else { 110 | w.WriteHeader(http.StatusOK) 111 | _, err := w.Write([]byte(tokenKeyJSON)) 112 | Expect(err).NotTo(HaveOccurred()) 113 | } 114 | }) 115 | keys, _ := a.TokenKeys() 116 | Expect(called).To(Equal(2)) 117 | Expect(keys).To(HaveLen(1)) 118 | Expect(keys[0].Kty).To(Equal("RSA")) 119 | Expect(keys[0].E).To(Equal("AQAB")) 120 | Expect(keys[0].Use).To(Equal("sig")) 121 | Expect(keys[0].Kid).To(Equal("sha2-2017-01-20-key")) 122 | Expect(keys[0].Alg).To(Equal("RS256")) 123 | Expect(keys[0].Value).To(Equal("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyH6kYCP29faDAUPKtei3\nV/Zh8eCHyHRDHrD0iosvgHuaakK1AFHjD19ojuPiTQm8r8nEeQtHb6mDi1LvZ03e\nEWxpvWwFfFVtCyBqWr5wn6IkY+ZFXfERLn2NCn6sMVxcFV12sUtuqD+jrW8MnTG7\nhofQqxmVVKKsZiXCvUSzfiKxDgoiRuD3MJSoZ0nQTHVmYxlFHuhTEETuTqSPmOXd\n/xJBVRi5WYCjt1aKRRZEz04zVEBVhVkr2H84qcVJHcfXFu4JM6dg0nmTjgd5cZUN\ncwA1KhK2/Qru9N0xlk9FGD2cvrVCCPWFPvZ1W7U7PBWOSBBH6GergA+dk2vQr7Ho\nlQIDAQAB\n-----END PUBLIC KEY-----")) 124 | Expect(keys[0].N).To(Equal("AMh-pGAj9vX2gwFDyrXot1f2YfHgh8h0Qx6w9IqLL4B7mmpCtQBR4w9faI7j4k0JvK_JxHkLR2-pg4tS72dN3hFsab1sBXxVbQsgalq-cJ-iJGPmRV3xES59jQp-rDFcXBVddrFLbqg_o61vDJ0xu4aH0KsZlVSirGYlwr1Es34isQ4KIkbg9zCUqGdJ0Ex1ZmMZRR7oUxBE7k6kj5jl3f8SQVUYuVmAo7dWikUWRM9OM1RAVYVZK9h_OKnFSR3H1xbuCTOnYNJ5k44HeXGVDXMANSoStv0K7vTdMZZPRRg9nL61Qgj1hT72dVu1OzwVjkgQR-hnq4APnZNr0K-x6JU")) 125 | }) 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /uaa_internals_test.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sclevine/spec" 7 | "github.com/sclevine/spec/report" 8 | ) 9 | 10 | var suite spec.Suite 11 | 12 | func init() { 13 | suite = spec.New("uaa-internals", spec.Report(report.Terminal{})) 14 | suite("ensureTransport", testEnsureTransport) 15 | suite("contains", testContains) 16 | suite("URLWithPath", testURLWithPath) 17 | suite("api", testAPI) 18 | suite("uaaTransport", testUaaTransport) 19 | } 20 | 21 | func TestUAAInternals(t *testing.T) { 22 | suite.Run(t) 23 | } 24 | -------------------------------------------------------------------------------- /uaa_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sclevine/spec" 7 | "github.com/sclevine/spec/report" 8 | ) 9 | 10 | var suite spec.Suite 11 | 12 | func init() { 13 | suite = spec.New("uaa", spec.Report(report.Terminal{})) 14 | suite("new", testNew) 15 | suite("clientExtra", testClientExtra) 16 | suite("curl", testCurl) 17 | suite("groupsExtra", testGroupsExtra) 18 | suite("isHealthy", testIsHealthy) 19 | suite("info", testInfo) 20 | suite("me", testMe) 21 | suite("tokenKey", testTokenKey) 22 | suite("tokenKeys", testTokenKeys) 23 | suite("buildSubdomainURL", testBuildSubdomainURL) 24 | suite("users", testUsers) 25 | 26 | // Generated 27 | suite("client", testClient) 28 | suite("group", testGroup) 29 | suite("identityZone", testIdentityZone) 30 | suite("mfaProvider", testMFAProvider) 31 | suite("user", testUser) 32 | } 33 | 34 | func TestUAA(t *testing.T) { 35 | suite.Run(t) 36 | } 37 | -------------------------------------------------------------------------------- /uaa_transport.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httputil" 7 | "strings" 8 | ) 9 | 10 | type uaaTransport struct { 11 | base http.RoundTripper 12 | LoggingEnabled bool 13 | } 14 | 15 | func (t *uaaTransport) RoundTrip(req *http.Request) (*http.Response, error) { 16 | t.logRequest(req) 17 | 18 | authHeader := req.Header.Get("Authorization") 19 | if strings.HasPrefix(strings.ToLower(authHeader), "basic") { 20 | req.Header.Add("X-CF-ENCODED-CREDENTIALS", "true") 21 | } 22 | 23 | resp, err := t.base.RoundTrip(req) 24 | if err != nil { 25 | return resp, err 26 | } 27 | 28 | t.logResponse(resp) 29 | 30 | return resp, err 31 | } 32 | 33 | func (t *uaaTransport) logRequest(req *http.Request) { 34 | if t.LoggingEnabled { 35 | bytes, _ := httputil.DumpRequest(req, false) 36 | fmt.Printf("%s", string(bytes)) 37 | } 38 | } 39 | 40 | func (t *uaaTransport) logResponse(resp *http.Response) { 41 | if t.LoggingEnabled { 42 | bytes, _ := httputil.DumpResponse(resp, true) 43 | fmt.Printf("%s", string(bytes)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /uaa_transport_test.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | . "github.com/onsi/gomega" 8 | "github.com/sclevine/spec" 9 | ) 10 | 11 | type fakeTransport struct { 12 | roundtripper func(req *http.Request) 13 | } 14 | 15 | func (f *fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) { 16 | if f.roundtripper != nil { 17 | f.roundtripper(req) 18 | } 19 | return nil, nil 20 | } 21 | 22 | func testUaaTransport(t *testing.T, when spec.G, it spec.S) { 23 | var ( 24 | request *http.Request 25 | transport *uaaTransport 26 | ) 27 | it.Before(func() { 28 | RegisterTestingT(t) 29 | transport = &uaaTransport{ 30 | base: &fakeTransport{roundtripper: func(req *http.Request) { 31 | request = req 32 | }}, 33 | LoggingEnabled: false, 34 | } 35 | }) 36 | 37 | it("can identify a nil baseTransport", func() { 38 | a := API{} 39 | Expect(a.baseTransportIsNil()).To(BeTrue()) 40 | a.baseTransport = nil 41 | Expect(a.baseTransportIsNil()).To(BeTrue()) 42 | }) 43 | 44 | it("can identify a non-nil baseTransport", func() { 45 | a := API{baseTransport: &fakeTransport{}} 46 | Expect(a.baseTransportIsNil()).To(BeFalse()) 47 | }) 48 | 49 | it("adds X-CF-ENCODED-CREDENTIALS header when using basic auth", func() { 50 | req, _ := http.NewRequest("", "", nil) 51 | req.Header.Add("Authorization", "Basic ENCODEDCREDENTIALS") 52 | _, err := transport.RoundTrip(req) 53 | Expect(err).NotTo(HaveOccurred()) 54 | Expect(request).NotTo(BeNil()) 55 | Expect(request.Header.Get("X-CF-ENCODED-CREDENTIALS")).To(Equal("true")) 56 | }) 57 | 58 | it("does not add X-CF-ENCODED-CREDENTIALS header when not using basic auth", func() { 59 | req, _ := http.NewRequest("", "", nil) 60 | _, err := transport.RoundTrip(req) 61 | Expect(err).NotTo(HaveOccurred()) 62 | Expect(request).NotTo(BeNil()) 63 | Expect(request.Header.Get("X-CF-ENCODED-CREDENTIALS")).To(BeEmpty()) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /url.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "path" 7 | "strings" 8 | ) 9 | 10 | // BuildTargetURL returns a URL. If the target does not include a scheme, https 11 | // will be used. 12 | func BuildTargetURL(target string) (*url.URL, error) { 13 | if !strings.Contains(target, "://") { 14 | target = fmt.Sprintf("https://%s", target) 15 | } 16 | 17 | return url.Parse(target) 18 | } 19 | 20 | // BuildSubdomainURL returns a URL that optionally includes the zone ID as a host 21 | // prefix. If the target does not include a scheme, https will be used. 22 | func BuildSubdomainURL(target string, zoneID string) (*url.URL, error) { 23 | url, err := BuildTargetURL(target) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | if !strings.HasPrefix(url.Hostname(), zoneID) { 29 | url.Host = fmt.Sprintf("%s.%s", zoneID, url.Host) 30 | } 31 | 32 | return url, nil 33 | } 34 | 35 | // urlWithPath copies the URL and sets the path on the copy. 36 | func urlWithPath(u url.URL, p string) url.URL { 37 | u.Path = path.Join(u.Path, p) 38 | return u 39 | } 40 | -------------------------------------------------------------------------------- /url_internal_test.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "log" 5 | "net/url" 6 | "testing" 7 | 8 | . "github.com/onsi/gomega" 9 | "github.com/sclevine/spec" 10 | ) 11 | 12 | func testURLWithPath(t *testing.T, when spec.G, it spec.S) { 13 | it.Before(func() { 14 | RegisterTestingT(t) 15 | log.SetFlags(log.Lshortfile) 16 | }) 17 | 18 | it("returns a URL which retains the path", func() { 19 | url, err := url.Parse("http://example.com/uaa") 20 | Expect(url).NotTo(BeNil()) 21 | Expect(err).To(BeNil()) 22 | 23 | withPath := urlWithPath(*url, "path") 24 | Expect(withPath.String()).To(Equal("http://example.com/uaa/path")) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /url_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | uaa "github.com/cloudfoundry-community/go-uaa" 8 | . "github.com/onsi/gomega" 9 | "github.com/sclevine/spec" 10 | ) 11 | 12 | func testBuildSubdomainURL(t *testing.T, when spec.G, it spec.S) { 13 | it.Before(func() { 14 | RegisterTestingT(t) 15 | log.SetFlags(log.Lshortfile) 16 | }) 17 | 18 | it("returns a URL", func() { 19 | url, err := uaa.BuildSubdomainURL("http://test.example.com", "") 20 | Expect(err).NotTo(HaveOccurred()) 21 | Expect(url).NotTo(BeNil()) 22 | Expect(url.String()).To(Equal("http://test.example.com")) 23 | }) 24 | 25 | it("returns an error when the url is invalid", func() { 26 | url, err := uaa.BuildSubdomainURL("(*#&^@%$&%)", "") 27 | Expect(err).To(HaveOccurred()) 28 | Expect(url).To(BeNil()) 29 | }) 30 | 31 | when("the zone ID is set", func() { 32 | it("adds the zone ID as a prefix to the target", func() { 33 | testCases := []struct { 34 | target string 35 | zoneID string 36 | expected string 37 | }{ 38 | {"http://test.example.com", "zone1", "http://zone1.test.example.com"}, 39 | {"https://test.example.com", "zone1", "https://zone1.test.example.com"}, 40 | {"test.example.com", "zone1", "https://zone1.test.example.com"}, 41 | } 42 | for i := range testCases { 43 | url, err := uaa.BuildSubdomainURL(testCases[i].target, testCases[i].zoneID) 44 | Expect(err).NotTo(HaveOccurred()) 45 | Expect(url).NotTo(BeNil()) 46 | Expect(url.String()).To(Equal(testCases[i].expected)) 47 | } 48 | }) 49 | 50 | it("returns an error when the url is invalid", func() { 51 | url, err := uaa.BuildSubdomainURL("(*#&^@%$&%)", "zone1") 52 | Expect(err).To(HaveOccurred()) 53 | Expect(url).To(BeNil()) 54 | }) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /users.go: -------------------------------------------------------------------------------- 1 | package uaa 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | // UsersEndpoint is the path to the users resource. 14 | const UsersEndpoint string = "/Users" 15 | 16 | // Meta describes the version and timestamps for a resource. 17 | type Meta struct { 18 | Version int `json:"version,omitempty"` 19 | Created string `json:"created,omitempty"` 20 | LastModified string `json:"lastModified,omitempty"` 21 | } 22 | 23 | // UserName is a person's name. 24 | type UserName struct { 25 | FamilyName string `json:"familyName,omitempty"` 26 | GivenName string `json:"givenName,omitempty"` 27 | } 28 | 29 | // Email is an email address. 30 | type Email struct { 31 | Value string `json:"value,omitempty"` 32 | Primary *bool `json:"primary,omitempty"` 33 | } 34 | 35 | // UserGroup is a group that a user belongs to. 36 | type UserGroup struct { 37 | Value string `json:"value,omitempty"` 38 | Display string `json:"display,omitempty"` 39 | Type string `json:"type,omitempty"` 40 | } 41 | 42 | // Approval is a record of the user's explicit approval or rejection for an 43 | // application's request for delegated permissions. 44 | type Approval struct { 45 | UserID string `json:"userId,omitempty"` 46 | ClientID string `json:"clientId,omitempty"` 47 | Scope string `json:"scope,omitempty"` 48 | Status string `json:"status,omitempty"` 49 | LastUpdatedAt string `json:"lastUpdatedAt,omitempty"` 50 | ExpiresAt string `json:"expiresAt,omitempty"` 51 | } 52 | 53 | // PhoneNumber is a phone number for a user. 54 | type PhoneNumber struct { 55 | Value string `json:"value"` 56 | } 57 | 58 | // User is a UAA user 59 | // http://docs.cloudfoundry.org/api/uaa/version/4.14.0/index.html#get-3. 60 | type User struct { 61 | ID string `json:"id,omitempty"` 62 | Password string `json:"password,omitempty"` 63 | ExternalID string `json:"externalId,omitempty"` 64 | Meta *Meta `json:"meta,omitempty"` 65 | Username string `json:"userName,omitempty"` 66 | Name *UserName `json:"name,omitempty"` 67 | Emails []Email `json:"emails,omitempty"` 68 | Groups []UserGroup `json:"groups,omitempty"` 69 | Approvals []Approval `json:"approvals,omitempty"` 70 | PhoneNumbers []PhoneNumber `json:"phoneNumbers,omitempty"` 71 | Active *bool `json:"active,omitempty"` 72 | Verified *bool `json:"verified,omitempty"` 73 | Origin string `json:"origin,omitempty"` 74 | ZoneID string `json:"zoneId,omitempty"` 75 | PasswordLastModified string `json:"passwordLastModified,omitempty"` 76 | PreviousLogonTime int `json:"previousLogonTime,omitempty"` 77 | LastLogonTime int `json:"lastLogonTime,omitempty"` 78 | Schemas []string `json:"schemas,omitempty"` 79 | } 80 | 81 | // Identifier returns the field used to uniquely identify a User. 82 | func (u User) Identifier() string { 83 | return u.ID 84 | } 85 | 86 | // paginatedUserList is the response from the API for a single page of users. 87 | type paginatedUserList struct { 88 | Page 89 | Resources []User `json:"resources"` 90 | Schemas []string `json:"schemas"` 91 | } 92 | 93 | // GetUserByUsername gets the user with the given username 94 | // http://docs.cloudfoundry.org/api/uaa/version/4.14.0/index.html#list-with-attribute-filtering. 95 | func (a *API) GetUserByUsername(username, origin, attributes string) (*User, error) { 96 | if username == "" { 97 | return nil, errors.New("username cannot be blank") 98 | } 99 | 100 | filter := fmt.Sprintf(`userName eq "%v"`, username) 101 | help := fmt.Sprintf("user %v not found", username) 102 | 103 | if origin != "" { 104 | filter = fmt.Sprintf(`%s and origin eq "%v"`, filter, origin) 105 | help = fmt.Sprintf(`%s in origin %v`, help, origin) 106 | } 107 | 108 | users, err := a.ListAllUsers(filter, "", attributes, "") 109 | if err != nil { 110 | return nil, err 111 | } 112 | if len(users) == 0 { 113 | return nil, errors.New(help) 114 | } 115 | if len(users) > 1 && origin == "" { 116 | var foundOrigins []string 117 | for _, user := range users { 118 | foundOrigins = append(foundOrigins, user.Origin) 119 | } 120 | 121 | msgTmpl := "Found users with username %v in multiple origins %v." 122 | msg := fmt.Sprintf(msgTmpl, username, "["+strings.Join(foundOrigins, ", ")+"]") 123 | return nil, errors.New(msg) 124 | } 125 | return &users[0], nil 126 | } 127 | 128 | // DeactivateUser deactivates the user with the given user ID 129 | // http://docs.cloudfoundry.org/api/uaa/version/4.14.0/index.html#patch. 130 | func (a *API) DeactivateUser(userID string, userMetaVersion int) error { 131 | return a.setActive(false, userID, userMetaVersion) 132 | } 133 | 134 | // ActivateUser activates the user with the given user ID 135 | // http://docs.cloudfoundry.org/api/uaa/version/4.14.0/index.html#patch. 136 | func (a *API) ActivateUser(userID string, userMetaVersion int) error { 137 | return a.setActive(true, userID, userMetaVersion) 138 | } 139 | 140 | func (a *API) setActive(active bool, userID string, userMetaVersion int) error { 141 | if userID == "" { 142 | return errors.New("userID cannot be blank") 143 | } 144 | u := urlWithPath(*a.TargetURL, fmt.Sprintf("%s/%s", UsersEndpoint, userID)) 145 | user := &User{} 146 | user.Active = &active 147 | 148 | extraHeaders := map[string]string{"If-Match": strconv.Itoa(userMetaVersion)} 149 | j, err := json.Marshal(user) 150 | if err != nil { 151 | return err 152 | } 153 | return a.doJSONWithHeaders(http.MethodPatch, &u, extraHeaders, bytes.NewBuffer([]byte(j)), nil, true) 154 | } 155 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package uaa_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | const MarcusUserResponse = `{ 9 | "id" : "00000000-0000-0000-0000-000000000001", 10 | "externalID" : "marcus-user", 11 | "meta" : { 12 | "version" : 1, 13 | "created" : "2017-01-15T16:54:15.677Z", 14 | "lastModified" : "2017-08-15T16:54:15.677Z" 15 | }, 16 | "userName" : "marcus@stoicism.com", 17 | "name" : { 18 | "familyName" : "Aurelius", 19 | "givenName" : "Marcus" 20 | }, 21 | "emails" : [ { 22 | "value" : "marcus@stoicism.com", 23 | "primary" : false 24 | } ], 25 | "groups" : [ { 26 | "value" : "ac2ab20e-0a2d-4b68-82e4-817ee6b258b4", 27 | "display" : "philosophy.read", 28 | "type" : "DIRECT" 29 | }, { 30 | "value" : "110b2434-4a30-439b-b5fc-f4cf47fc04f0", 31 | "display" : "philosophy.write", 32 | "type" : "DIRECT" 33 | }], 34 | "approvals" : [ { 35 | "userID" : "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", 36 | "clientID" : "shinyclient", 37 | "scope" : "philosophy.read", 38 | "status" : "APPROVED", 39 | "lastUpdatedAt" : "2017-08-15T16:54:15.765Z", 40 | "expiresAt" : "2017-08-15T16:54:25.765Z" 41 | }, { 42 | "userID" : "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", 43 | "clientID" : "identity", 44 | "scope" : "uaa.user", 45 | "status" : "APPROVED", 46 | "lastUpdatedAt" : "2017-08-15T16:54:45.767Z", 47 | "expiresAt" : "2017-08-15T16:54:45.767Z" 48 | } ], 49 | "phoneNumbers" : [ { 50 | "value" : "5555555555" 51 | } ], 52 | "active" : true, 53 | "verified" : true, 54 | "origin" : "uaa", 55 | "zoneID" : "uaa", 56 | "passwordLastModified" : "2017-08-15T16:54:15.000Z", 57 | "previousLogonTime" : 1502816055768, 58 | "lastLogonTime" : 1502816055768, 59 | "schemas" : [ "urn:scim:schemas:core:1.0" ] 60 | }` 61 | 62 | const DrSeussUserResponse = `{ 63 | "id" : "00000000-0000-0000-0000-000000000002", 64 | "externalID" : "seuss-user", 65 | "meta" : { 66 | "version" : 1, 67 | "created" : "2017-01-15T16:54:15.677Z", 68 | "lastModified" : "2017-08-15T16:54:15.677Z" 69 | }, 70 | "userName" : "drseuss@whoville.com", 71 | "name" : { 72 | "familyName" : "Theodore", 73 | "givenName" : "Giesel" 74 | }, 75 | "emails" : [ { 76 | "value" : "drseuss@whoville.com", 77 | "primary" : true 78 | } ], 79 | "groups" : [ { 80 | "value" : "ac2ab20e-0a2d-4b68-82e4-817ee6b258b4", 81 | "display" : "cat_in_hat.read", 82 | "type" : "DIRECT" 83 | }, { 84 | "value" : "110b2434-4a30-439b-b5fc-f4cf47fc04f0", 85 | "display" : "cat_in_hat.write", 86 | "type" : "DIRECT" 87 | }], 88 | "approvals" : [ { 89 | "userID" : "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", 90 | "clientID" : "shinyclient", 91 | "scope" : "cat_in_hat.read", 92 | "status" : "APPROVED", 93 | "lastUpdatedAt" : "2017-08-15T16:54:15.765Z", 94 | "expiresAt" : "2017-08-15T16:54:25.765Z" 95 | }, { 96 | "userID" : "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", 97 | "clientID" : "identity", 98 | "scope" : "cat_in_hat.write", 99 | "status" : "APPROVED", 100 | "lastUpdatedAt" : "2017-08-15T16:54:45.767Z", 101 | "expiresAt" : "2017-08-15T16:54:45.767Z" 102 | } ], 103 | "phoneNumbers" : [ { 104 | "value" : "5555555555" 105 | } ], 106 | "active" : true, 107 | "verified" : true, 108 | "origin" : "uaa", 109 | "zoneID" : "uaa", 110 | "passwordLastModified" : "2017-08-15T16:54:15.000Z", 111 | "previousLogonTime" : 1502816055768, 112 | "lastLogonTime" : 1502816055768, 113 | "schemas" : [ "urn:scim:schemas:core:1.0" ] 114 | }` 115 | 116 | const PaginatedResponseTmpl = `{ 117 | "resources": [%v,%v], 118 | "startIndex" : 1, 119 | "itemsPerPage" : 50, 120 | "totalResults" : 2, 121 | "schemas" : [ "urn:scim:schemas:core:1.0"] 122 | }` 123 | 124 | func MultiPaginatedResponse(startIndex, itemsPerPage, totalResults int, resources ...interface{}) string { 125 | bytes, _ := json.Marshal(resources) 126 | 127 | return fmt.Sprintf(`{ 128 | "resources": %v, 129 | "startIndex" : %d, 130 | "itemsPerPage" : %d, 131 | "totalResults" : %d, 132 | "schemas" : [ "urn:scim:schemas:core:1.0"] 133 | }`, string(bytes), startIndex, itemsPerPage, totalResults) 134 | } 135 | 136 | func PaginatedResponse(resources ...interface{}) string { 137 | bytes, _ := json.Marshal(resources) 138 | 139 | return fmt.Sprintf(`{ 140 | "resources": %v, 141 | "startIndex" : 1, 142 | "itemsPerPage" : 50, 143 | "totalResults" : %v, 144 | "schemas" : [ "urn:scim:schemas:core:1.0"] 145 | }`, string(bytes), len(resources)) 146 | } 147 | --------------------------------------------------------------------------------