├── .cfignore ├── .codeclimate.yml ├── .github └── workflows │ └── security-considerations.yml ├── .gitignore ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── acceptance-tests.sh ├── acceptance-tests.yml ├── broker.go ├── broker_test.go ├── cf.go ├── config.json ├── credentials.example.yml ├── go.mod ├── go.sum ├── main.go ├── main_suite_test.go ├── manifest.yml ├── mocks └── PAASClient.go ├── password.go ├── password_test.go ├── pipeline.yml ├── run-tests.sh ├── run-tests.yml ├── uaa.go ├── utils.go └── utils_test.go /.cfignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: false 5 | fixme: 6 | enabled: true 7 | govet: 8 | enabled: true 9 | golint: 10 | enabled: true 11 | 12 | ratings: 13 | paths: 14 | - "**.go" 15 | -------------------------------------------------------------------------------- /.github/workflows/security-considerations.yml: -------------------------------------------------------------------------------- 1 | name: Security Considerations 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, reopened] 6 | branches: [main, master, develop] 7 | 8 | jobs: 9 | security-considerations: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: cloud-gov/security-considerations-action@main 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Output of the go coverage tool, specifically when used with LiteIDE 27 | *.out 28 | 29 | # external packages folder 30 | vendor/ 31 | .idea/* -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cloud-gov/customer-success-squad 2 | 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Welcome! 2 | 3 | We're so glad you're thinking about contributing to an 18F open source project! If you're unsure about anything, just ask -- or submit the issue or pull request anyway. The worst that can happen is you'll be politely asked to change something. We love all friendly contributions. 4 | 5 | We want to ensure a welcoming environment for all of our projects. Our staff follow the [18F Code of Conduct](https://github.com/18F/code-of-conduct/blob/master/code-of-conduct.md) and all contributors should do the same. 6 | 7 | We encourage you to read this project's CONTRIBUTING policy (you are here), its [LICENSE](LICENSE.md), and its [README](README.md). 8 | 9 | If you have any questions or want to read more, check out the [18F Open Source Policy GitHub repository](https://github.com/18f/open-source-policy), or just [shoot us an email](mailto:18f@gsa.gov). 10 | 11 | ## Public domain 12 | 13 | This project is in the public domain within the United States, and 14 | copyright and related rights in the work worldwide are waived through 15 | the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 16 | 17 | All contributions to this project will be released under the CC0 18 | dedication. By submitting a pull request, you are agreeing to comply 19 | with this waiver of copyright interest. 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | As a work of the United States government, this project is in the 2 | public domain within the United States. 3 | 4 | Additionally, we waive copyright and related rights in the work 5 | worldwide through the CC0 1.0 Universal public domain dedication. 6 | 7 | ## CC0 1.0 Universal summary 8 | 9 | This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 10 | 11 | ### No copyright 12 | 13 | The person who associated a work with this deed has dedicated the work to 14 | the public domain by waiving all of his or her rights to the work worldwide 15 | under copyright law, including all related and neighboring rights, to the 16 | extent allowed by law. 17 | 18 | You can copy, modify, distribute and perform the work, even for commercial 19 | purposes, all without asking permission. 20 | 21 | ### Other information 22 | 23 | In no way are the patent or trademark rights of any person affected by CC0, 24 | nor are the rights that other persons may have in the work or in how the 25 | work is used, such as publicity or privacy rights. 26 | 27 | Unless expressly stated otherwise, the person who associated a work with 28 | this deed makes no warranties about the work, and disclaims liability for 29 | all uses of the work, to the fullest extent permitted by applicable law. 30 | When using or citing the work, you should not imply endorsement by the 31 | author or the affirmer. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cloud Foundry UAA Credentials Broker 2 | ===================================== 3 | [![Code Climate](https://codeclimate.com/github/cloud-gov/uaa-credentials-broker/badges/gpa.svg)](https://codeclimate.com/github/cloud-gov/uaa-credentials-broker) 4 | 5 | This service broker allows Cloud Foundry users to provision and deprovision UAA users and clients: 6 | 7 | * UAA users managed by the broker are scoped to a given organization and space and can be used to push applications when password authentication is needed--such as when deploying from a continuous integration service. Live example: [the cloud.gov service account service](https://cloud.gov/docs/services/cloud-gov-service-account/). 8 | * UAA clients can be used to leverage UAA authentication in tenant applications. Live example: [leveraging cloud.gov authentication](https://cloud.gov/docs/management/leveraging-authentication/) using the [cloud.gov identity provider service](https://cloud.gov/docs/services/cloud-gov-identity-provider/). 9 | 10 | ## Usage 11 | 12 | ### UAA users 13 | 14 | * Create service instance: 15 | 16 | ```bash 17 | $ cf create-service cloud-gov-service-account space-deployer my-service-account 18 | ``` 19 | 20 | * Create service key: 21 | 22 | ```bash 23 | $ cf create-service-key my-service-account my-service-key 24 | ``` 25 | 26 | * Retrieve credentials from service key: 27 | 28 | ```bash 29 | $ cf service-key my-service-account my-service-key 30 | ``` 31 | 32 | * To rotate or deprovision when user is no longer needed, delete the service key: 33 | 34 | ```bash 35 | $ cf delete-service-key my-service-account my-service-key 36 | ``` 37 | 38 | ### UAA clients 39 | 40 | * Create a service instance: 41 | 42 | ```bash 43 | $ cf create-service cloud-gov-identity-provider oauth-client my-uaa-client 44 | ``` 45 | 46 | * Create service key: 47 | 48 | ```bash 49 | $ cf create-service-key my-uaa-client my-service-key \ 50 | -c '{"redirect_uri": ["https://my.app.cloud.gov/auth/callback"]}' 51 | ``` 52 | 53 | * Retrieve credentials from service key: 54 | 55 | ```bash 56 | $ cf service-key my-uaa-client my-service-key 57 | ``` 58 | 59 | * To rotate or deprovision when client is no longer needed, delete the service key: 60 | 61 | ```bash 62 | $ cf delete-service-key my-uaa-client my-service-key 63 | ``` 64 | 65 | ## Deployment 66 | 67 | * Create UAA client: 68 | 69 | ```bash 70 | $ uaac client add uaa-credentials-broker \ 71 | --name uaa-credentials-broker \ 72 | --authorized_grant_types client_credentials \ 73 | --authorities scim.write,uaa.admin,cloud_controller.admin \ 74 | --scope uaa.none 75 | ``` 76 | 77 | * Update Concourse pipeline: 78 | 79 | ```bash 80 | fly -t ci set-pipeline -p uaa-credentials-broker -c pipeline.yml -l credentials.yml 81 | ``` 82 | 83 | ## Public domain 84 | 85 | This project is in the worldwide [public domain](LICENSE.md). As stated in [CONTRIBUTING](CONTRIBUTING.md): 86 | 87 | > This project is in the public domain within the United States, and copyright and related rights in the work worldwide are waived through the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 88 | > 89 | > All contributions to this project will be released under the CC0 dedication. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest. 90 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | **Reporting Security Issues** 3 | 4 | Please refrain from reporting security vulnerabilities through public GitHub issues. 5 | 6 | Instead, kindly report them via the information provided in [cloud.gov's security.txt](https://cloud.gov/.well-known/security.txt). 7 | 8 | When reporting, include the following details (as much as possible) to help us understand the nature and extent of the potential issue: 9 | 10 | - Type of issue (e.g., buffer overflow, SQL injection, cross-site scripting, etc.) 11 | - Full paths of related source file(s) 12 | - Location of affected source code (tag/branch/commit or direct URL) 13 | - Any special configuration required to reproduce the issue 14 | - Step-by-step instructions to reproduce the issue 15 | - Proof-of-concept or exploit code (if available) 16 | - Impact of the issue, including potential exploitation by attackers 17 | 18 | Providing this information will facilitate a quicker triage of your report. 19 | -------------------------------------------------------------------------------- /acceptance-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -u 4 | 5 | cf login -a $CF_API_URL -u $CF_USERNAME -p $CF_PASSWORD -o $CF_ORGANIZATION -s $CF_SPACE &> /dev/null 6 | 7 | uaac target $UAA_API_URL &> /dev/null 8 | uaac token client get $UAA_CLIENT_ID -s $UAA_CLIENT_SECRET &> /dev/null 9 | 10 | function test_cloud-gov-service-account-plan () { 11 | local plan=$1 # from config.json 12 | local role=$2 # in cloud foundry: space_developer | space_auditor 13 | 14 | local passed_count=0 15 | local fail_count=0 16 | local results_file="cloud-gov-service-account-plan_${plan}" 17 | rm -f $results_file 18 | 19 | local svc_instance_name="${plan}-acceptance-instance" 20 | local svc_key_name="${plan}-acceptance-key" 21 | 22 | # Create service 23 | cf create-service "cloud-gov-service-account" "$plan" "$svc_instance_name" &> /dev/null 24 | cf create-service-key "$svc_instance_name" "$svc_key_name" &> /dev/null 25 | 26 | # Service Key GUID should be the username 27 | local svc_key_guid=$(cf service-key "$svc_instance_name" "$svc_key_name" --guid) 28 | local username=$(cf curl "/v3/service_credential_bindings/${svc_key_guid}/details" | jq -r '.credentials.username') 29 | if [ "$svc_key_guid" != "$username" ]; then 30 | echo "FAIL: Username ${username} does not match service key guid ${svc_key_guid}." >> $results_file 31 | fail_count=$((fail_count+1)) 32 | else 33 | echo "PASSED: Username ${username} matches service key guid." >> $results_file 34 | passed_count=$((passed_count+1)) 35 | fi 36 | 37 | # User exists in UAA 38 | local uaa_user=$(uaac user get "${username}" -a id | grep "id:") 39 | if [ -z "$uaa_user" ]; then 40 | echo "FAIL: Expected user ${username} to exist in UAA." >> $results_file 41 | fail_count=$((fail_count+1)) 42 | else 43 | echo "PASSED: User ${username} exists in UAA." >> $results_file 44 | passed_count=$((passed_count+1)) 45 | fi 46 | 47 | # User has the role 48 | local space_guid=$(cf space --guid "$CF_SPACE") 49 | local user_guid=$(cf curl "/v3/users?usernames=${username}" | jq -r '.resources[].guid' ) 50 | local role_count=$(cf curl "/v3/roles?space_guids=${space_guid}&user_guids=${user_guid}&types=${role}" | jq -r '.pagination.total_results') 51 | if [ 0 -eq $role_count ]; then 52 | echo "FAIL: User ${username} is not a ${role} in ${CF_ORGANIZATION}/${CF_SPACE}." >> $results_file 53 | fail_count=$((fail_count+1)) 54 | else 55 | echo "PASSED: User ${username} is a ${role} in ${CF_ORGANIZATION}/${CF_SPACE}." >> $results_file 56 | passed_count=$((passed_count+1)) 57 | fi 58 | 59 | # Delete service instance 60 | cf delete-service-key -f "$svc_instance_name" "$svc_key_name" &> /dev/null 61 | cf delete-service -f "$svc_instance_name" &> /dev/null 62 | 63 | # User does not exist in CF 64 | local user_count=$(cf curl "/v3/users?usernames=${username}" | jq -r '.pagination.total_results' ) 65 | if [ 0 -ne $user_count ]; then 66 | echo "FAIL: User ${username} should not exist in Cloud Foundry." >> $results_file 67 | fail_count=$((fail_count+1)) 68 | else 69 | echo "PASSED: User ${username} no longer exists in Cloud Foundry." >> $results_file 70 | passed_count=$((passed_count+1)) 71 | fi 72 | 73 | # User does not exist in UAA 74 | local uaa_user=$(uaac user get "${username}" -a id | grep "id:") 75 | if [ ! -z "$uaa_user" ]; then 76 | echo "FAIL: User ${username} should not exist in UAA." >> $results_file 77 | fail_count=$((fail_count+1)) 78 | else 79 | echo "PASSED: User ${username} does not exist in UAA." >> $results_file 80 | passed_count=$((passed_count+1)) 81 | fi 82 | 83 | echo "Offering: cloud-gov-service-account. Plan: $plan. ${passed_count} passed. ${fail_count} failed." 84 | cat $results_file 85 | echo " " 86 | 87 | rm -f $results_file 88 | 89 | if [ $fail_count -gt 0 ]; then 90 | return 1 91 | fi 92 | } 93 | 94 | 95 | function test_cloud-gov-service-account_offering () { 96 | test_cloud-gov-service-account-plan "space-auditor" "space_auditor" 97 | local space_auditor_return_code=$? 98 | test_cloud-gov-service-account-plan "space-deployer" "space_developer" 99 | local space_developer_return_code=$? 100 | 101 | if [ $space_auditor_return_code -gt 0 ] || [ $space_developer_return_code -gt 0 ]; then 102 | return 1 103 | else 104 | return 0 105 | fi 106 | } 107 | 108 | function test_oauth-client_plan () { 109 | local allow_public=$1 110 | 111 | local passed_count=0 112 | local fail_count=0 113 | local results_file="cloud-gov-identity-provider_oauth-client" 114 | rm -f $results_file 115 | 116 | local svc_instance_name="oauth-client-test-instance" 117 | local svc_key_name="oauth-client-test-key" 118 | local redirect_uri="https://cloud.gov" 119 | 120 | cf create-service "cloud-gov-identity-provider" "oauth-client" "$svc_instance_name" &> /dev/null 121 | 122 | if [ "$allow_public" == true ]; then 123 | cf create-service-key "$svc_instance_name" "$svc_key_name" -c '{"redirect_uri": ["'"$redirect_uri"'"], "allowpublic": true}' &> /dev/null 124 | else 125 | cf create-service-key "$svc_instance_name" "$svc_key_name" -c '{"redirect_uri": ["'"$redirect_uri"'"]}' &> /dev/null 126 | fi 127 | 128 | # Service Key GUID should be the client id 129 | local svc_key_guid=$(cf service-key "$svc_instance_name" "$svc_key_name" --guid) 130 | local client_id=$(cf curl "/v3/service_credential_bindings/${svc_key_guid}/details" | jq -r '.credentials.client_id') 131 | if [ "$svc_key_guid" != "$client_id" ]; then 132 | echo "FAIL: Client ID ${client_id} does not macth service key guid ${svc_key_guid}." >> $results_file 133 | fail_count=$((fail_count+1)) 134 | else 135 | echo "PASSED: Client ID ${client_id} matches service key guid." >> $results_file 136 | passed_count=$((passed_count+1)) 137 | fi 138 | 139 | local uaa_client_record=$(uaac client get "${client_id}") 140 | # client id in uaa 141 | local uaa_client_id=$(echo "$uaa_client_record" | grep "client_id: $client_id") 142 | if [ -z "$uaa_client_id" ]; then 143 | echo "FAIL: Expected client ${client_id} to exist in UAA." >> $results_file 144 | fail_count=$((fail_count+1)) 145 | else 146 | echo "PASSED: Client ${client_id} exists in UAA." >> $results_file 147 | passed_count=$((passed_count+1)) 148 | fi 149 | 150 | # redirect URI in UAA 151 | local uaa_client_redirect_uri=$(echo "$uaa_client_record" | grep "redirect_uri: $redirect_uri") 152 | if [ -z "$uaa_client_redirect_uri" ]; then 153 | echo "FAIL: Expected redirect_uri ${redirect_uri} to be set in UAA." >> $results_file 154 | fail_count=$((fail_count+1)) 155 | else 156 | echo "PASSED: redirect_uri ${redirect_uri} set in UAA." >> $results_file 157 | passed_count=$((passed_count+1)) 158 | fi 159 | 160 | # Allowpublic set correctly 161 | local uaa_allowpublic=$(echo "$uaa_client_record" | grep "allowpublic: $allow_public") 162 | if [ "$allow_public" = true ] && [ ! -z "$uaa_allowpublic" ]; then 163 | echo "PASSED: allowpublic correctly set to true in UAA." >> $results_file 164 | passed_count=$((passed_count+1)) 165 | elif [ "$allow_public" = false ] && [ -z "$uaa_allowpublic" ]; then 166 | echo "PASSED: allowpublic correctly set to false in UAA." >> $results_file 167 | passed_count=$((passed_count+1)) 168 | else 169 | echo "FAIL: allowpublic incorrectly set in UAA. Expected $allow_public." >> $results_file 170 | fail_count=$((fail_count+1)) 171 | fi 172 | 173 | # Delete service instance 174 | cf delete-service-key -f "$svc_instance_name" "$svc_key_name" &> /dev/null 175 | cf delete-service -f "$svc_instance_name" &> /dev/null 176 | 177 | # Client does not exist in UAA 178 | uaa_client_id=$(uaac client get "${client_id}" -a client_id | grep "client_id: ${client_id}") 179 | if [ -z "$uaa_client_id" ]; then 180 | echo "PASSED: Client ${client_id} removed from UAA." >> $results_file 181 | passed_count=$((passed_count+1)) 182 | else 183 | echo "FAIL: Client ${client_id} not removed from UAA." >> $results_file 184 | fail_count=$((fail_count+1)) 185 | fi 186 | 187 | echo "Offering: cloud-gov-identity-provider. Plan: oauth-client. Allow Public: $allow_public. ${passed_count} passed. ${fail_count} failed." 188 | cat $results_file 189 | echo " " 190 | 191 | rm -f $results_file 192 | 193 | if [ $fail_count -gt 0 ]; then 194 | return 1 195 | fi 196 | 197 | } 198 | 199 | function test_cloud-gov-identity-provider_offering () { 200 | test_oauth-client_plan false 201 | local nopublic=$? 202 | test_oauth-client_plan true 203 | local allowpublic=$? 204 | 205 | if [ $nopublic -gt 0 ] || [ $allowpublic -gt 0 ]; then 206 | return 1 207 | else 208 | return 0 209 | fi 210 | 211 | } 212 | 213 | test_cloud-gov-service-account_offering 214 | cloud_gov_service_account=$? 215 | test_cloud-gov-identity-provider_offering 216 | cloud_gov_identity_provider=$? 217 | 218 | if [ $cloud_gov_service_account -gt 0 ] || [ $cloud_gov_identity_provider -gt 0 ]; then 219 | exit 1 220 | else 221 | exit 0 222 | fi -------------------------------------------------------------------------------- /acceptance-tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | inputs: 5 | - name: broker-src 6 | 7 | run: 8 | path: broker-src/acceptance-tests.sh 9 | 10 | params: 11 | CF_API_URL: 12 | CF_USERNAME: 13 | CF_PASSWORD: 14 | CF_ORGANIZATION: 15 | CF_SPACE: 16 | UAA_API_URL: 17 | UAA_CLIENT_ID: 18 | UAA_CLIENT_SECRET: 19 | 20 | -------------------------------------------------------------------------------- /broker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | 9 | "code.cloudfoundry.org/lager" 10 | "github.com/pivotal-cf/brokerapi" 11 | 12 | "io/ioutil" 13 | "os" 14 | "path/filepath" 15 | "strings" 16 | ) 17 | 18 | type BindOptions struct { 19 | RedirectURI []string `json:"redirect_uri"` 20 | Scopes []string `json:"scopes"` 21 | AllowPublic *bool `json:"allowpublic"` 22 | } 23 | 24 | var ( 25 | clientAccountGUID = "6b508bb8-2af7-4a75-9efd-7b76a01d705d" 26 | userAccountGUID = "964bd86d-72fa-4852-957f-e4cd802de34b" 27 | deployerGUID = "074e652b-b77b-4ac3-8d5b-52144486b1a3" 28 | auditorGUID = "dc3a6d48-9622-434a-b418-1d920193b575" 29 | ) 30 | 31 | var ( 32 | defaultScopes = []string{"openid"} 33 | allowedScopes = map[string]bool{ 34 | "openid": true, 35 | } 36 | ) 37 | 38 | type DeployerAccountBroker struct { 39 | uaaClient AuthClient 40 | cfClient PAASClient 41 | generatePassword PasswordGenerator 42 | logger lager.Logger 43 | config Config 44 | } 45 | 46 | func (b *DeployerAccountBroker) Services(context context.Context) []brokerapi.Service { 47 | var services []brokerapi.Service 48 | pwd, _ := os.Getwd() 49 | 50 | buf, err := ioutil.ReadFile(filepath.Join(pwd, "config.json")) 51 | if err != nil { 52 | b.logger.Error("services", err) 53 | return []brokerapi.Service{} 54 | } 55 | err = json.Unmarshal(buf, &services) 56 | if err != nil { 57 | return []brokerapi.Service{} 58 | } 59 | return services 60 | } 61 | 62 | func (b *DeployerAccountBroker) Provision( 63 | context context.Context, 64 | instanceID string, 65 | details brokerapi.ProvisionDetails, 66 | asyncAllowed bool, 67 | ) (brokerapi.ProvisionedServiceSpec, error) { 68 | return brokerapi.ProvisionedServiceSpec{}, nil 69 | } 70 | 71 | func (b *DeployerAccountBroker) Deprovision( 72 | context context.Context, 73 | instanceID string, 74 | details brokerapi.DeprovisionDetails, 75 | asyncAllowed bool, 76 | ) (brokerapi.DeprovisionServiceSpec, error) { 77 | // Handle instances created before credential management was moved to bind and unbind 78 | switch details.ServiceID { 79 | case clientAccountGUID: 80 | if err := b.deleteClient(instanceID); err != nil { 81 | return brokerapi.DeprovisionServiceSpec{}, err 82 | } 83 | case userAccountGUID: 84 | user, err := b.uaaClient.GetUser(instanceID) 85 | if err != nil { 86 | if strings.Contains(err.Error(), "got 0") { 87 | return brokerapi.DeprovisionServiceSpec{}, nil 88 | } 89 | return brokerapi.DeprovisionServiceSpec{}, err 90 | } 91 | 92 | err = b.cfClient.DeleteUser(user.ID) 93 | if err != nil { 94 | return brokerapi.DeprovisionServiceSpec{}, err 95 | } 96 | 97 | err = b.uaaClient.DeleteUser(user.ID) 98 | if err != nil { 99 | return brokerapi.DeprovisionServiceSpec{}, err 100 | } 101 | default: 102 | return brokerapi.DeprovisionServiceSpec{}, fmt.Errorf("Service ID %s not found", details.ServiceID) 103 | } 104 | 105 | return brokerapi.DeprovisionServiceSpec{}, nil 106 | } 107 | 108 | func parseBindOptions(details brokerapi.BindDetails) (BindOptions, error) { 109 | opts := BindOptions{} 110 | 111 | if len(details.RawParameters) == 0 { 112 | return opts, errors.New(`must pass JSON configuration with field "redirect_uri"`) 113 | } 114 | 115 | if err := json.Unmarshal(details.RawParameters, &opts); err != nil { 116 | return opts, err 117 | } 118 | 119 | if len(opts.RedirectURI) == 0 { 120 | return opts, errors.New(`must pass field "redirect_uri"`) 121 | } 122 | 123 | return opts, nil 124 | } 125 | 126 | func (b *DeployerAccountBroker) Bind( 127 | context context.Context, 128 | instanceID, bindingID string, 129 | details brokerapi.BindDetails, 130 | ) (brokerapi.Binding, error) { 131 | password := b.generatePassword(b.config.PasswordLength) 132 | 133 | switch details.ServiceID { 134 | case clientAccountGUID: 135 | opts, err := parseBindOptions(details) 136 | if err != nil { 137 | return brokerapi.Binding{}, err 138 | } 139 | 140 | if _, err := b.provisionClient(bindingID, password, opts); err != nil { 141 | return brokerapi.Binding{}, err 142 | } 143 | 144 | return brokerapi.Binding{ 145 | Credentials: map[string]string{ 146 | "client_id": bindingID, 147 | "client_secret": password, 148 | }, 149 | }, nil 150 | case userAccountGUID: 151 | instance, err := b.cfClient.ServiceInstanceByGuid(instanceID) 152 | if err != nil { 153 | return brokerapi.Binding{}, err 154 | } 155 | 156 | space, err := b.cfClient.GetSpaceByGuid(instance.Relationships.Space.Data.GUID) 157 | if err != nil { 158 | return brokerapi.Binding{}, err 159 | } 160 | 161 | user, err := b.provisionUser(bindingID, password) 162 | if err != nil { 163 | return brokerapi.Binding{}, err 164 | } 165 | _, err = b.cfClient.CreateUser(user.ID) 166 | if err != nil { 167 | return brokerapi.Binding{}, err 168 | } 169 | 170 | _, err = b.cfClient.AssociateOrgUserByUsername(space.Relationships.Organization.Data.GUID, user.UserName) 171 | if err != nil { 172 | return brokerapi.Binding{}, err 173 | } 174 | 175 | switch details.PlanID { 176 | case deployerGUID: 177 | _, err = b.cfClient.AssociateSpaceDeveloperByUsername(instance.Relationships.Space.Data.GUID, user.UserName) 178 | if err != nil { 179 | return brokerapi.Binding{}, err 180 | } 181 | case auditorGUID: 182 | _, err = b.cfClient.AssociateSpaceAuditorByUsername(instance.Relationships.Space.Data.GUID, user.UserName) 183 | if err != nil { 184 | return brokerapi.Binding{}, err 185 | } 186 | } 187 | 188 | return brokerapi.Binding{ 189 | Credentials: map[string]string{ 190 | "username": bindingID, 191 | "password": password, 192 | }, 193 | }, nil 194 | default: 195 | return brokerapi.Binding{}, fmt.Errorf("Service ID %s not found", details.ServiceID) 196 | } 197 | 198 | return brokerapi.Binding{}, nil 199 | } 200 | 201 | func (b *DeployerAccountBroker) Unbind( 202 | context context.Context, 203 | instanceID, 204 | bindingID string, 205 | details brokerapi.UnbindDetails, 206 | ) error { 207 | switch details.ServiceID { 208 | case clientAccountGUID: 209 | err := b.deleteClient(bindingID) 210 | if err != nil { 211 | return err 212 | } 213 | case userAccountGUID: 214 | user, err := b.uaaClient.GetUser(bindingID) 215 | if err != nil { 216 | if strings.Contains(err.Error(), "got 0") { 217 | return nil 218 | } 219 | return err 220 | } 221 | 222 | err = b.cfClient.DeleteUser(user.ID) 223 | if err != nil { 224 | return err 225 | } 226 | 227 | err = b.uaaClient.DeleteUser(user.ID) 228 | if err != nil { 229 | return err 230 | } 231 | default: 232 | return fmt.Errorf("Service ID %s not found", details.ServiceID) 233 | } 234 | 235 | return nil 236 | } 237 | 238 | func (b *DeployerAccountBroker) Update(context context.Context, instanceID string, details brokerapi.UpdateDetails, asyncAllowed bool) (brokerapi.UpdateServiceSpec, error) { 239 | return brokerapi.UpdateServiceSpec{}, errors.New("Broker does not support update") 240 | } 241 | 242 | func (b *DeployerAccountBroker) LastOperation(context context.Context, instanceID, operationData string) (brokerapi.LastOperation, error) { 243 | return brokerapi.LastOperation{}, errors.New("Broker does not support last operation") 244 | } 245 | 246 | func (b *DeployerAccountBroker) provisionClient( 247 | clientID, 248 | clientSecret string, 249 | opts BindOptions, 250 | ) (Client, error) { 251 | var scopes = opts.Scopes 252 | if len(opts.Scopes) == 0 { 253 | scopes = defaultScopes 254 | } 255 | forbiddenScopes := []string{} 256 | for _, scope := range scopes { 257 | if _, ok := allowedScopes[scope]; !ok { 258 | forbiddenScopes = append(forbiddenScopes, scope) 259 | } 260 | } 261 | if len(forbiddenScopes) > 0 { 262 | return Client{}, fmt.Errorf("Scope(s) not permitted: %s", strings.Join(forbiddenScopes, ", ")) 263 | } 264 | 265 | client := Client{ 266 | ID: clientID, 267 | AuthorizedGrantTypes: []string{"authorization_code", "refresh_token"}, 268 | Scope: scopes, 269 | RedirectURI: opts.RedirectURI, 270 | ClientSecret: clientSecret, 271 | AccessTokenValidity: b.config.AccessTokenValidity, 272 | RefreshTokenValidity: b.config.RefreshTokenValidity, 273 | } 274 | 275 | if opts.AllowPublic != nil { 276 | client.AllowPublic = *opts.AllowPublic 277 | } 278 | 279 | return b.uaaClient.CreateClient(client) 280 | } 281 | 282 | func (b *DeployerAccountBroker) deleteClient( 283 | clientID string, 284 | ) error { 285 | err := b.uaaClient.DeleteClient(clientID) 286 | 287 | // Allow 404 responses on deletion 288 | if err != nil && strings.Contains(err.Error(), "404") { 289 | return nil 290 | } 291 | 292 | return err 293 | } 294 | 295 | func (b *DeployerAccountBroker) provisionUser(userID, password string) (User, error) { 296 | user := User{ 297 | UserName: userID, 298 | Password: password, 299 | Emails: []Email{{ 300 | Value: b.config.EmailAddress, 301 | Primary: true, 302 | }}, 303 | } 304 | 305 | return b.uaaClient.CreateUser(user) 306 | } 307 | -------------------------------------------------------------------------------- /broker_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "code.cloudfoundry.org/lager/lagertest" 8 | cf "github.com/cloudfoundry/go-cfclient/v3/resource" 9 | "github.com/pivotal-cf/brokerapi" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | "github.com/stretchr/testify/mock" 14 | 15 | "github.com/cloud-gov/uaa-credentials-broker/mocks" 16 | ) 17 | 18 | type FakeUAAClient struct { 19 | mock.Mock 20 | userGUID string 21 | userName string 22 | clientGUID string 23 | } 24 | 25 | func (c *FakeUAAClient) CreateClient(client Client) (Client, error) { 26 | c.Called(client) 27 | return Client{ID: c.clientGUID}, nil 28 | } 29 | 30 | func (c *FakeUAAClient) DeleteClient(clientID string) error { 31 | args := c.Called(clientID) 32 | return args.Error(0) 33 | } 34 | 35 | func (c *FakeUAAClient) GetUser(userID string) (User, error) { 36 | c.Called(userID) 37 | return User{ID: c.userGUID}, nil 38 | } 39 | 40 | func (c *FakeUAAClient) CreateUser(user User) (User, error) { 41 | c.Called(user) 42 | return User{ID: c.userGUID, UserName: c.userName}, nil 43 | } 44 | 45 | func (c *FakeUAAClient) DeleteUser(userID string) error { 46 | c.Called(userID) 47 | return nil 48 | } 49 | 50 | var _ = Describe("broker", func() { 51 | var ( 52 | uaaClient FakeUAAClient 53 | cfClient mocks.PAASClient 54 | broker DeployerAccountBroker 55 | ) 56 | 57 | BeforeEach(func() { 58 | uaaClient = FakeUAAClient{userGUID: "user-guid", userName: "binding-guid"} 59 | cfClient = mocks.PAASClient{} 60 | broker = DeployerAccountBroker{ 61 | uaaClient: &uaaClient, 62 | cfClient: &cfClient, 63 | logger: lagertest.NewTestLogger("broker-test"), 64 | generatePassword: func(int) string { 65 | return "password" 66 | }, 67 | config: Config{ 68 | EmailAddress: "fake@fake.org", 69 | PasswordLength: 32, 70 | AccessTokenValidity: 600, 71 | RefreshTokenValidity: 86400, 72 | }, 73 | } 74 | }) 75 | 76 | Describe("uaa client", func() { 77 | Describe("parse options", func() { 78 | It("returns error options when no parameters are specified", func() { 79 | options, err := parseBindOptions(brokerapi.BindDetails{ 80 | RawParameters: []byte(``), 81 | }) 82 | Expect(err).To(HaveOccurred()) 83 | Expect(options).To(Equal(BindOptions{})) 84 | }) 85 | 86 | It("returns error options when no redirect URI specified", func() { 87 | options, err := parseBindOptions(brokerapi.BindDetails{ 88 | RawParameters: []byte(`{"redirect_uri":[]}`), 89 | }) 90 | Expect(err).To(HaveOccurred()) 91 | Expect(options).To(Equal(BindOptions{ 92 | RedirectURI: []string{}, 93 | })) 94 | }) 95 | 96 | It("returns options with redirect URI", func() { 97 | options, err := parseBindOptions(brokerapi.BindDetails{ 98 | RawParameters: []byte(`{"redirect_uri":["example.com"]}`), 99 | }) 100 | Expect(err).NotTo(HaveOccurred()) 101 | Expect(options).To(Equal(BindOptions{ 102 | RedirectURI: []string{"example.com"}, 103 | })) 104 | }) 105 | 106 | It("returns options with scopes", func() { 107 | options, err := parseBindOptions(brokerapi.BindDetails{ 108 | RawParameters: []byte(`{"redirect_uri":["example.com"], "scopes":["scope1"]}`), 109 | }) 110 | Expect(err).NotTo(HaveOccurred()) 111 | Expect(options).To(Equal(BindOptions{ 112 | RedirectURI: []string{"example.com"}, 113 | Scopes: []string{"scope1"}, 114 | })) 115 | }) 116 | 117 | It("returns options with allowpublic", func() { 118 | options, err := parseBindOptions(brokerapi.BindDetails{ 119 | RawParameters: []byte(`{"redirect_uri":["example.com"], "allowpublic": true}`), 120 | }) 121 | Expect(err).NotTo(HaveOccurred()) 122 | allowPublicTrue := true 123 | Expect(options).To(Equal(BindOptions{ 124 | RedirectURI: []string{"example.com"}, 125 | AllowPublic: &allowPublicTrue, 126 | })) 127 | }) 128 | }) 129 | 130 | Describe("provision", func() { 131 | It("returns a binding", func() { 132 | uaaClient.On("CreateClient", Client{ 133 | ID: "binding-guid", 134 | AuthorizedGrantTypes: []string{"authorization_code", "refresh_token"}, 135 | Scope: []string{"openid"}, 136 | RedirectURI: []string{"https://cloud.gov"}, 137 | ClientSecret: "password", 138 | AccessTokenValidity: 600, 139 | RefreshTokenValidity: 86400, 140 | }).Return(Client{ID: "client-guid"}, nil) 141 | 142 | _, err := broker.Bind( 143 | context.Background(), 144 | "instance-guid", 145 | "binding-guid", 146 | brokerapi.BindDetails{ 147 | AppGUID: "app-guid", 148 | ServiceID: clientAccountGUID, 149 | RawParameters: []byte(`{"redirect_uri": ["https://cloud.gov"]}`), 150 | }, 151 | ) 152 | Expect(err).NotTo(HaveOccurred()) 153 | cfClient.AssertExpectations(GinkgoT()) 154 | uaaClient.AssertExpectations(GinkgoT()) 155 | }) 156 | 157 | It("errors if params missing", func() { 158 | uaaClient.On("CreateClient", Client{ 159 | ID: "binding-guid", 160 | AuthorizedGrantTypes: []string{"authorization_code", "refresh_token"}, 161 | Scope: []string{"openid"}, 162 | RedirectURI: []string{"https://cloud.gov"}, 163 | ClientSecret: "password", 164 | AccessTokenValidity: 600, 165 | RefreshTokenValidity: 86400, 166 | }).Return(Client{ID: "client-guid"}, nil) 167 | 168 | _, err := broker.Bind( 169 | context.Background(), 170 | "instance-guid", 171 | "binding-guid", 172 | brokerapi.BindDetails{ 173 | AppGUID: "app-guid", 174 | ServiceID: clientAccountGUID, 175 | }, 176 | ) 177 | Expect(err).To(HaveOccurred()) 178 | Expect(err.Error()).To(Equal(`must pass JSON configuration with field "redirect_uri"`)) 179 | }) 180 | 181 | It("errors if params incomplete", func() { 182 | uaaClient.On("CreateClient", Client{ 183 | ID: "binding-guid", 184 | AuthorizedGrantTypes: []string{"authorization_code", "refresh_token"}, 185 | Scope: []string{"openid"}, 186 | RedirectURI: []string{"https://cloud.gov"}, 187 | ClientSecret: "password", 188 | AccessTokenValidity: 600, 189 | RefreshTokenValidity: 86400, 190 | }).Return(Client{ID: "client-guid"}, nil) 191 | 192 | _, err := broker.Bind( 193 | context.Background(), 194 | "instance-guid", 195 | "binding-guid", 196 | brokerapi.BindDetails{ 197 | AppGUID: "app-guid", 198 | ServiceID: clientAccountGUID, 199 | RawParameters: []byte(`{}`), 200 | }, 201 | ) 202 | Expect(err).To(HaveOccurred()) 203 | Expect(err.Error()).To(Equal(`must pass field "redirect_uri"`)) 204 | }) 205 | 206 | It("accepts allowed scopes", func() { 207 | uaaClient.On("CreateClient", Client{ 208 | ID: "binding-guid", 209 | AuthorizedGrantTypes: []string{"authorization_code", "refresh_token"}, 210 | Scope: []string{"openid"}, 211 | RedirectURI: []string{"https://cloud.gov"}, 212 | ClientSecret: "password", 213 | AccessTokenValidity: 600, 214 | RefreshTokenValidity: 86400, 215 | }).Return(Client{ID: "client-guid"}, nil) 216 | 217 | _, err := broker.Bind( 218 | context.Background(), 219 | "instance-guid", 220 | "binding-guid", 221 | brokerapi.BindDetails{ 222 | AppGUID: "app-guid", 223 | ServiceID: clientAccountGUID, 224 | RawParameters: []byte(`{"redirect_uri": ["https://cloud.gov"], "scopes": ["openid"]}`), 225 | }, 226 | ) 227 | Expect(err).NotTo(HaveOccurred()) 228 | cfClient.AssertExpectations(GinkgoT()) 229 | uaaClient.AssertExpectations(GinkgoT()) 230 | }) 231 | 232 | It("rejects forbidden scopes", func() { 233 | _, err := broker.Bind( 234 | context.Background(), 235 | "instance-guid", 236 | "binding-guid", 237 | brokerapi.BindDetails{ 238 | AppGUID: "app-guid", 239 | ServiceID: clientAccountGUID, 240 | RawParameters: []byte(`{"redirect_uri": ["https://cloud.gov"], "scopes": ["cloud_controller.write"]}`), 241 | }, 242 | ) 243 | Expect(err).To(HaveOccurred()) 244 | Expect(err.Error()).To(Equal("Scope(s) not permitted: cloud_controller.write")) 245 | }) 246 | 247 | It("uses specified allowpublic value", func() { 248 | uaaClient.On("CreateClient", Client{ 249 | ID: "binding-guid", 250 | AuthorizedGrantTypes: []string{"authorization_code", "refresh_token"}, 251 | Scope: []string{"openid"}, 252 | RedirectURI: []string{"https://cloud.gov"}, 253 | ClientSecret: "password", 254 | AccessTokenValidity: 600, 255 | RefreshTokenValidity: 86400, 256 | AllowPublic: true, 257 | }).Return(Client{ID: "client-guid"}, nil) 258 | 259 | _, err := broker.Bind( 260 | context.Background(), 261 | "instance-guid", 262 | "binding-guid", 263 | brokerapi.BindDetails{ 264 | AppGUID: "app-guid", 265 | ServiceID: clientAccountGUID, 266 | RawParameters: []byte(`{"redirect_uri": ["https://cloud.gov"], "scopes": ["openid"], "allowpublic": true}`), 267 | }, 268 | ) 269 | Expect(err).NotTo(HaveOccurred()) 270 | cfClient.AssertExpectations(GinkgoT()) 271 | uaaClient.AssertExpectations(GinkgoT()) 272 | }) 273 | }) 274 | 275 | Describe("unbind", func() { 276 | It("does not return an error", func() { 277 | uaaClient.On("DeleteClient", "binding-guid").Return(nil) 278 | 279 | err := broker.Unbind( 280 | context.Background(), 281 | "instance-guid", 282 | "binding-guid", 283 | brokerapi.UnbindDetails{ 284 | ServiceID: clientAccountGUID, 285 | }, 286 | ) 287 | Expect(err).NotTo(HaveOccurred()) 288 | uaaClient.AssertExpectations(GinkgoT()) 289 | }) 290 | 291 | It("does not return an error for a 404 response on deletion", func() { 292 | uaaClient.On("DeleteClient", "binding-guid2").Return(fmt.Errorf("Expected status 200; got: %d", 404)) 293 | 294 | err := broker.Unbind( 295 | context.Background(), 296 | "instance-guid", 297 | "binding-guid2", 298 | brokerapi.UnbindDetails{ 299 | ServiceID: clientAccountGUID, 300 | }, 301 | ) 302 | Expect(err).NotTo(HaveOccurred()) 303 | uaaClient.AssertExpectations(GinkgoT()) 304 | }) 305 | 306 | It("does return an error for a response other than 200/404 on deletion", func() { 307 | uaaClient.On("DeleteClient", "binding-guid3").Return(fmt.Errorf("Expected status 200; got: %d", 500)) 308 | 309 | err := broker.Unbind( 310 | context.Background(), 311 | "instance-guid", 312 | "binding-guid3", 313 | brokerapi.UnbindDetails{ 314 | ServiceID: clientAccountGUID, 315 | }, 316 | ) 317 | Expect(err).To(HaveOccurred()) 318 | uaaClient.AssertExpectations(GinkgoT()) 319 | }) 320 | }) 321 | 322 | Describe("deprovision", func() { 323 | It("does not return an error", func() { 324 | uaaClient.On("DeleteClient", "instance-guid").Return(nil) 325 | 326 | _, err := broker.Deprovision( 327 | context.Background(), 328 | "instance-guid", 329 | brokerapi.DeprovisionDetails{ 330 | ServiceID: clientAccountGUID, 331 | }, 332 | false, 333 | ) 334 | Expect(err).NotTo(HaveOccurred()) 335 | uaaClient.AssertExpectations(GinkgoT()) 336 | }) 337 | }) 338 | 339 | It("does not return an error for a 404 response on deletion", func() { 340 | uaaClient.On("DeleteClient", "instance-guid2").Return(fmt.Errorf("Expected status 200; got: %d", 404)) 341 | 342 | _, err := broker.Deprovision( 343 | context.Background(), 344 | "instance-guid2", 345 | brokerapi.DeprovisionDetails{ 346 | ServiceID: clientAccountGUID, 347 | }, 348 | false, 349 | ) 350 | Expect(err).NotTo(HaveOccurred()) 351 | uaaClient.AssertExpectations(GinkgoT()) 352 | }) 353 | 354 | It("does return an error for a response other than 200/404 on deletion", func() { 355 | uaaClient.On("DeleteClient", "instance-guid3").Return(fmt.Errorf("Expected status 200; got: %d", 500)) 356 | 357 | _, err := broker.Deprovision( 358 | context.Background(), 359 | "instance-guid3", 360 | brokerapi.DeprovisionDetails{ 361 | ServiceID: clientAccountGUID, 362 | }, 363 | false, 364 | ) 365 | Expect(err).To(HaveOccurred()) 366 | uaaClient.AssertExpectations(GinkgoT()) 367 | }) 368 | }) 369 | 370 | Describe("uaa user", func() { 371 | Describe("provision", func() { 372 | svcInst := &cf.ServiceInstance{ 373 | Relationships: cf.ServiceInstanceRelationships{ 374 | Space: &cf.ToOneRelationship{ 375 | Data: &cf.Relationship{ 376 | GUID: "space-guid", 377 | }, 378 | }, 379 | }, 380 | } 381 | space := &cf.Space{ 382 | Relationships: &cf.SpaceRelationships{ 383 | Organization: &cf.ToOneRelationship{ 384 | Data: &cf.Relationship{ 385 | GUID: "org-guid", 386 | }, 387 | }, 388 | }, 389 | } 390 | user := &cf.User{} 391 | user.GUID = "user-guid" 392 | It("returns a provision service spec for space-deployer", func() { 393 | cfClient.On("ServiceInstanceByGuid", "instance-guid").Return(svcInst, nil) 394 | cfClient.On("GetSpaceByGuid", "space-guid").Return(space, nil) 395 | uaaClient.On("CreateUser", User{ 396 | UserName: "binding-guid", 397 | Password: "password", 398 | Emails: []Email{{ 399 | Value: "fake@fake.org", 400 | Primary: true, 401 | }}, 402 | }).Return(User{ID: "user-guid"}, nil) 403 | cfClient.On("CreateUser", "user-guid").Return(user, nil) 404 | cfClient.On("AssociateOrgUserByUsername", "org-guid", "binding-guid").Return(&cf.Role{}, nil) 405 | cfClient.On("AssociateSpaceDeveloperByUsername", "space-guid", "binding-guid").Return(&cf.Role{}, nil) 406 | 407 | _, err := broker.Bind( 408 | context.Background(), 409 | "instance-guid", 410 | "binding-guid", 411 | brokerapi.BindDetails{ 412 | AppGUID: "app-guid", 413 | ServiceID: userAccountGUID, 414 | PlanID: deployerGUID, 415 | }, 416 | ) 417 | Expect(err).NotTo(HaveOccurred()) 418 | uaaClient.AssertExpectations(GinkgoT()) 419 | cfClient.AssertExpectations(GinkgoT()) 420 | }) 421 | 422 | It("returns a provision service spec for space-auditor", func() { 423 | cfClient.On("ServiceInstanceByGuid", "instance-guid").Return(svcInst, nil) 424 | cfClient.On("GetSpaceByGuid", "space-guid").Return(space, nil) 425 | uaaClient.On("CreateUser", User{ 426 | UserName: "binding-guid", 427 | Password: "password", 428 | Emails: []Email{{ 429 | Value: "fake@fake.org", 430 | Primary: true, 431 | }}, 432 | }).Return(User{ID: "user-guid"}, nil) 433 | cfClient.On("CreateUser", "user-guid").Return(user, nil) 434 | cfClient.On("AssociateOrgUserByUsername", "org-guid", "binding-guid").Return(&cf.Role{}, nil) 435 | cfClient.On("AssociateSpaceAuditorByUsername", "space-guid", "binding-guid").Return(&cf.Role{}, nil) 436 | 437 | _, err := broker.Bind( 438 | context.Background(), 439 | "instance-guid", 440 | "binding-guid", 441 | brokerapi.BindDetails{ 442 | AppGUID: "app-guid", 443 | ServiceID: userAccountGUID, 444 | PlanID: auditorGUID, 445 | }, 446 | ) 447 | Expect(err).NotTo(HaveOccurred()) 448 | uaaClient.AssertExpectations(GinkgoT()) 449 | cfClient.AssertExpectations(GinkgoT()) 450 | }) 451 | }) 452 | 453 | Describe("deprovision", func() { 454 | It("returns a deprovision service spec", func() { 455 | uaaClient.On("GetUser", "binding-guid").Return(User{ID: "user-guid"}, nil) 456 | uaaClient.On("DeleteUser", "user-guid").Return(nil) 457 | cfClient.On("DeleteUser", "user-guid").Return(nil) 458 | 459 | err := broker.Unbind( 460 | context.Background(), 461 | "instance-guid", 462 | "binding-guid", 463 | brokerapi.UnbindDetails{ 464 | ServiceID: userAccountGUID, 465 | }, 466 | ) 467 | Expect(err).NotTo(HaveOccurred()) 468 | uaaClient.AssertExpectations(GinkgoT()) 469 | cfClient.AssertExpectations(GinkgoT()) 470 | }) 471 | }) 472 | }) 473 | }) 474 | -------------------------------------------------------------------------------- /cf.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | cfclient "github.com/cloudfoundry/go-cfclient/v3/client" 7 | cf "github.com/cloudfoundry/go-cfclient/v3/resource" 8 | ) 9 | 10 | type PAASClient interface { 11 | ServiceInstanceByGuid(guid string) (*cf.ServiceInstance, error) 12 | GetSpaceByGuid(guid string) (*cf.Space, error) 13 | CreateUser(guid string) (*cf.User, error) 14 | DeleteUser(guid string) error 15 | AssociateOrgUserByUsername(orgID, userName string) (*cf.Role, error) 16 | AssociateOrgAuditorByUsername(orgID, userName string) (*cf.Role, error) 17 | AssociateSpaceDeveloperByUsername(spaceID, userName string) (*cf.Role, error) 18 | AssociateSpaceAuditorByUsername(spaceID, userName string) (*cf.Role, error) 19 | } 20 | 21 | type CFClient struct { 22 | Client *cfclient.Client 23 | } 24 | 25 | func (c *CFClient) ServiceInstanceByGuid(guid string) (*cf.ServiceInstance, error) { 26 | svcInst, err := c.Client.ServiceInstances.Get(context.Background(), guid) 27 | return svcInst, err 28 | } 29 | 30 | func (c *CFClient) GetSpaceByGuid(guid string) (*cf.Space, error) { 31 | space, err := c.Client.Spaces.Get(context.Background(), guid) 32 | return space, err 33 | } 34 | 35 | func (c *CFClient) GetOrganizationByGuid(guid string) (*cf.Organization, error) { 36 | org, err := c.Client.Organizations.Get(context.Background(), guid) 37 | return org, err 38 | } 39 | 40 | func (c *CFClient) CreateUser(guid string) (*cf.User, error) { 41 | user, err := c.Client.Users.Create(context.Background(), &cf.UserCreate{GUID: guid}) 42 | return user, err 43 | } 44 | 45 | func (c *CFClient) DeleteUser(guid string) error { 46 | _, err := c.Client.Users.Delete(context.Background(), guid) 47 | return err 48 | } 49 | 50 | func (c *CFClient) AssociateOrgUserByUsernameAndRole(orgID, userName string, roleType cf.OrganizationRoleType) (*cf.Role, error) { 51 | role, err := c.Client.Roles.CreateOrganizationRoleWithUsername(context.Background(), orgID, userName, roleType, "") 52 | return role, err 53 | } 54 | 55 | func (c *CFClient) AssociateOrgUserByUsername(orgID, userName string) (*cf.Role, error) { 56 | return c.AssociateOrgUserByUsernameAndRole(orgID, userName, cf.OrganizationRoleUser) 57 | } 58 | 59 | func (c *CFClient) AssociateOrgAuditorByUsername(orgID, userName string) (*cf.Role, error) { 60 | return c.AssociateOrgUserByUsernameAndRole(orgID, userName, cf.OrganizationRoleAuditor) 61 | } 62 | 63 | func (c *CFClient) AssociateSpaceUserByUsernameAndRole(spaceID, userName string, roleType cf.SpaceRoleType) (*cf.Role, error) { 64 | role, err := c.Client.Roles.CreateSpaceRoleWithUsername(context.Background(), spaceID, userName, roleType, "") 65 | return role, err 66 | } 67 | 68 | func (c *CFClient) AssociateSpaceDeveloperByUsername(spaceID, userName string) (*cf.Role, error) { 69 | return c.AssociateSpaceUserByUsernameAndRole(spaceID, userName, cf.SpaceRoleDeveloper) 70 | } 71 | 72 | func (c *CFClient) AssociateSpaceAuditorByUsername(spaceID, userName string) (*cf.Role, error) { 73 | return c.AssociateSpaceUserByUsernameAndRole(spaceID, userName, cf.SpaceRoleAuditor) 74 | } 75 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "6b508bb8-2af7-4a75-9efd-7b76a01d705d", 4 | "name": "cloud-gov-identity-provider", 5 | "description": "Manage client credentials for authenticating cloud.gov users in your app", 6 | "bindable": true, 7 | "metadata": { 8 | "documentationUrl": "https://cloud.gov/docs/services/cloud-gov-identity-provider/" 9 | }, 10 | "plans": [ 11 | { 12 | "id": "e6fd8aaa-b5ba-4b19-b52e-44c18ab8ca1d", 13 | "name": "oauth-client", 14 | "description": "OAuth client credentials for authenticating cloud.gov users in your app" 15 | } 16 | ] 17 | }, 18 | { 19 | "id": "964bd86d-72fa-4852-957f-e4cd802de34b", 20 | "name": "cloud-gov-service-account", 21 | "description": "Manage cloud.gov service accounts with access to your organization", 22 | "bindable": true, 23 | "metadata": { 24 | "documentationUrl": "https://cloud.gov/docs/services/cloud-gov-service-account/" 25 | }, 26 | "plans": [ 27 | { 28 | "id": "074e652b-b77b-4ac3-8d5b-52144486b1a3", 29 | "name": "space-deployer", 30 | "description": "A service account for continuous deployment, limited to a single space" 31 | }, 32 | { 33 | "id": "dc3a6d48-9622-434a-b418-1d920193b575", 34 | "name": "space-auditor", 35 | "description": "A service account for auditing configuration and monitoring events limited to a single space" 36 | } 37 | ] 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /credentials.example.yml: -------------------------------------------------------------------------------- 1 | broker-git-url: https://github.com/cloud-gov/uaa-credentials-broker 2 | broker-git-branch: master 3 | 4 | pipeline-tasks-git-url: https://github.com/18F/cg-pipeline-tasks 5 | pipeline-tasks-git-branch: master 6 | 7 | uaa-address-staging: 8 | uaa-client-id-staging: 9 | uaa-client-secret-staging: 10 | uaa-zone-staging: 11 | broker-username-staging: 12 | broker-password-staging: 13 | email-address-staging: 14 | fugacious-address-staging: 15 | 16 | cf-api-url-staging: 17 | cf-deploy-username-staging: 18 | cf-deploy-password-staging: 19 | cf-organization-staging: 20 | cf-space-staging: 21 | 22 | cf-deploy-username-test-staging: 23 | cf-deploy-password-test-staging: 24 | cf-organization-test-staging: 25 | cf-space-test-staging: 26 | uaa-client-id-test-staging: 27 | uaa-client-secret-test-staging: 28 | 29 | uaa-address-production: 30 | uaa-client-id-production: 31 | uaa-client-secret-production: 32 | uaa-zone-production: 33 | broker-username-production: 34 | broker-password-production: 35 | email-address-production: 36 | fugacious-address-production: 37 | 38 | cf-api-url-production: 39 | cf-deploy-username-production: 40 | cf-deploy-password-production: 41 | cf-organization-production: 42 | cf-space-production: 43 | 44 | cf-deploy-username-test-production: 45 | cf-deploy-password-test-production: 46 | cf-organization-test-production: 47 | cf-space-test-production: 48 | uaa-client-id-test-production: 49 | uaa-client-secret-test-production: 50 | 51 | slack-channel: 52 | slack-username: 53 | slack-icon-url: 54 | slack-webhook-url: 55 | 56 | service-account-blacklist: 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloud-gov/uaa-credentials-broker 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | code.cloudfoundry.org/lager v0.0.0-20170612214856-dfcbcba2dd4a 9 | github.com/cloudfoundry/go-cfclient/v3 v3.0.0-alpha.12 10 | github.com/kelseyhightower/envconfig v1.2.0 11 | github.com/onsi/ginkgo v1.16.5 12 | github.com/onsi/gomega v1.37.0 13 | github.com/pivotal-cf/brokerapi v0.0.0-20170523133650-6d25b9398d9f 14 | github.com/stretchr/testify v1.10.0 15 | golang.org/x/oauth2 v0.29.0 16 | ) 17 | 18 | require ( 19 | code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f // indirect 20 | github.com/Masterminds/semver v1.4.2 // indirect 21 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/drewolson/testflight v1.0.0 // indirect 24 | github.com/fsnotify/fsnotify v1.4.9 // indirect 25 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab // indirect 26 | github.com/golang/protobuf v1.5.0 // indirect 27 | github.com/google/go-cmp v0.7.0 // indirect 28 | github.com/gorilla/context v1.1.1 // indirect 29 | github.com/gorilla/mux v0.0.0-20160816184630-cf79e51a62d8 // indirect 30 | github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 // indirect 31 | github.com/nxadm/tail v1.4.8 // indirect 32 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect 33 | github.com/pborman/uuid v1.2.1 // indirect 34 | github.com/pkg/errors v0.8.1 // indirect 35 | github.com/pmezard/go-difflib v1.0.0 // indirect 36 | github.com/stretchr/objx v0.5.2 // indirect 37 | golang.org/x/net v0.38.0 // indirect 38 | golang.org/x/sys v0.31.0 // indirect 39 | golang.org/x/text v0.23.0 // indirect 40 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 41 | google.golang.org/appengine v1.4.0 // indirect 42 | google.golang.org/protobuf v1.36.5 // indirect 43 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 44 | gopkg.in/yaml.v2 v2.4.0 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk= 3 | code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI= 4 | code.cloudfoundry.org/lager v0.0.0-20170612214856-dfcbcba2dd4a h1:u/48b1SarJKVUcJmVO2RRKo9Hi/r80tiS8qLKWfOry4= 5 | code.cloudfoundry.org/lager v0.0.0-20170612214856-dfcbcba2dd4a/go.mod h1:O2sS7gKP3HM2iemG+EnwvyNQK7pTSC6Foi4QiMp9sSk= 6 | github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= 7 | github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 8 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 9 | github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= 10 | github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s= 11 | github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14= 12 | github.com/cloudfoundry/go-cfclient/v3 v3.0.0-alpha.12 h1:6ejqaobIjUY+HJWrwUW1dqiGz7s4PlG/fIDznCZwlS8= 13 | github.com/cloudfoundry/go-cfclient/v3 v3.0.0-alpha.12/go.mod h1:JmRWZTZEEup+5BlR+YYhzPUfJABidYEpIBNS10KjXqk= 14 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= 15 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/drewolson/testflight v1.0.0 h1:jgA0pHcFIPnXoBmyFzrdoR2ka4UvReMDsjYc7Jcvl80= 20 | github.com/drewolson/testflight v1.0.0/go.mod h1:t9oKuuEohRGLb80SWX+uxJHuhX98B7HnojqtW+Ryq30= 21 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 22 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 23 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 24 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= 25 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= 26 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 27 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 29 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 30 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 31 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 32 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 33 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 34 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= 35 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 36 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 37 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 38 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 39 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 40 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 42 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 43 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 44 | github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= 45 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 46 | github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f h1:FDM3EtwZLyhW48YRiyqjivNlNZjAObv4xt4NnJaU+NQ= 47 | github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 48 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 49 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 50 | github.com/gorilla/mux v0.0.0-20160816184630-cf79e51a62d8 h1:I8uk/hpK+5UVhrha/fbkkMLN2PwwklRyEOYg0hQugHo= 51 | github.com/gorilla/mux v0.0.0-20160816184630-cf79e51a62d8/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 52 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 53 | github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= 54 | github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 55 | github.com/kelseyhightower/envconfig v1.2.0 h1:ShuWkCxhdgKvpbfMMuCPjAKfdMDS/iClYwdQDByknVk= 56 | github.com/kelseyhightower/envconfig v1.2.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 57 | github.com/kr/pretty v0.0.0-20160823170715-cfb55aafdaf3/go.mod h1:Bvhd+E3laJ0AVkG0c9rmtZcnhV0HQ3+c3YxxqTvc/gA= 58 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 59 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 60 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 61 | github.com/kr/text v0.0.0-20160504234017-7cafcd837844/go.mod h1:sjUstKUATFIcff4qlB53Kml0wQPtJVc/3fWrmuUmcfA= 62 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 63 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 64 | github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw= 65 | github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI= 66 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 67 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 68 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 69 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 70 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 71 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 72 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 73 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 74 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 75 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 76 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 77 | github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 78 | github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 79 | github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= 80 | github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= 81 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= 82 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= 83 | github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= 84 | github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 85 | github.com/pivotal-cf/brokerapi v0.0.0-20170523133650-6d25b9398d9f h1:ssBCyJ+V65ag7/P+01GtvVLYzKrxAu8pq4K/PqYKapg= 86 | github.com/pivotal-cf/brokerapi v0.0.0-20170523133650-6d25b9398d9f/go.mod h1:P+oA8NvkCTkq2t4DohBiyqQo69Ub15RKGcm/vKNP0gg= 87 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 88 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 89 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 90 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 91 | github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470 h1:R0uuDVEvfDha2O6dfJRr4/5NBHKEbZhMPZmqOWpEkSo= 92 | github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 93 | github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo= 94 | github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= 95 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 96 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 97 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 98 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 99 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 100 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 101 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 102 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 103 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 104 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 105 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 106 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 107 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 108 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 109 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 110 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 111 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 112 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 113 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 114 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 115 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 116 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 117 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 118 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 119 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 120 | golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 121 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 122 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 123 | golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 124 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 125 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 126 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 127 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 129 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 131 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 132 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 134 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 140 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 141 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 142 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 143 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 144 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 145 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 146 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 147 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 148 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 149 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 150 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 151 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 152 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 153 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 154 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 155 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 156 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 157 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 158 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 159 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 160 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 161 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 162 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 163 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 164 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 165 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 166 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 167 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 168 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 169 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 170 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 171 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 172 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 173 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 174 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 175 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 177 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 178 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | cfclient "github.com/cloudfoundry/go-cfclient/v3/client" 11 | cfconfig "github.com/cloudfoundry/go-cfclient/v3/config" 12 | 13 | "code.cloudfoundry.org/lager" 14 | "github.com/kelseyhightower/envconfig" 15 | "github.com/pivotal-cf/brokerapi" 16 | 17 | "golang.org/x/oauth2" 18 | "golang.org/x/oauth2/clientcredentials" 19 | ) 20 | 21 | type Config struct { 22 | UAAAddress string `envconfig:"uaa_address" required:"true"` 23 | UAAClientID string `envconfig:"uaa_client_id" required:"true"` 24 | UAAClientSecret string `envconfig:"uaa_client_secret" required:"true"` 25 | UAAZone string `envconfig:"uaa_zone" default:"uaa"` 26 | CFAddress string `envconfig:"cf_address" required:"true"` 27 | BrokerUsername string `envconfig:"broker_username" required:"true"` 28 | BrokerPassword string `envconfig:"broker_password" required:"true"` 29 | PasswordLength int `envconfig:"password_length" default:"32"` 30 | EmailAddress string `envconfig:"email_address" required:"true"` 31 | AccessTokenValidity int `envconfig:"access_token_validity" default:"600"` 32 | RefreshTokenValidity int `envconfig:"refresh_token_validity" default:"86400"` 33 | Port string `envconfig:"port" default:"3000"` 34 | } 35 | 36 | func NewClient(config Config) *http.Client { 37 | ctx := context.WithValue(context.Background(), oauth2.HTTPClient, http.DefaultClient) 38 | cfg := &clientcredentials.Config{ 39 | ClientID: config.UAAClientID, 40 | ClientSecret: config.UAAClientSecret, 41 | TokenURL: fmt.Sprintf("%s/oauth/token", config.UAAAddress), 42 | } 43 | return cfg.Client(ctx) 44 | } 45 | 46 | func main() { 47 | logger := lager.NewLogger("uaa-credentials-broker") 48 | logger.RegisterSink(lager.NewWriterSink(os.Stderr, lager.INFO)) 49 | 50 | config := Config{} 51 | err := envconfig.Process("", &config) 52 | if err != nil { 53 | log.Fatalf("%s", err) 54 | } 55 | 56 | client := NewClient(config) 57 | 58 | cfConfig, _ := cfconfig.New(config.CFAddress, cfconfig.ClientCredentials(config.UAAClientID, config.UAAClientSecret)) 59 | cfClient, err := cfclient.New(cfConfig) 60 | if err != nil { 61 | log.Fatalf("%s", err) 62 | } 63 | paasClient := &CFClient{ 64 | Client: cfClient, 65 | } 66 | 67 | broker := DeployerAccountBroker{ 68 | logger: logger, 69 | uaaClient: &UAAClient{ 70 | logger: logger, 71 | endpoint: config.UAAAddress, 72 | zone: config.UAAZone, 73 | client: client, 74 | }, 75 | cfClient: paasClient, 76 | generatePassword: GenerateSecurePassword, 77 | config: config, 78 | } 79 | credentials := brokerapi.BrokerCredentials{ 80 | Username: config.BrokerUsername, 81 | Password: config.BrokerPassword, 82 | } 83 | 84 | brokerAPI := brokerapi.New(&broker, logger, credentials) 85 | http.Handle("/", brokerAPI) 86 | http.ListenAndServe(fmt.Sprintf(":%s", config.Port), nil) 87 | } 88 | -------------------------------------------------------------------------------- /main_suite_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestMain(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Main Suite") 13 | } 14 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: uaa-credentials-broker 4 | command: uaa-credentials-broker 5 | buildpacks: 6 | - https://github.com/cloudfoundry/go-buildpack 7 | env: 8 | GOVERSION: go1.22 9 | -------------------------------------------------------------------------------- /mocks/PAASClient.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | cf "github.com/cloudfoundry/go-cfclient/v3/resource" 5 | mock "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | // PAASClient is an autogenerated mock type for the PAASClient type 9 | type PAASClient struct { 10 | mock.Mock 11 | } 12 | 13 | // AssociateOrgAuditorByUsername provides a mock function with given fields: orgID, userName 14 | func (_m *PAASClient) AssociateOrgAuditorByUsername(orgID string, userName string) (*cf.Role, error) { 15 | ret := _m.Called(orgID, userName) 16 | 17 | var r0 *cf.Role 18 | if rf, ok := ret.Get(0).(func(string, string) *cf.Role); ok { 19 | r0 = rf(orgID, userName) 20 | } else { 21 | r0 = ret.Get(0).(*cf.Role) 22 | } 23 | 24 | var r1 error 25 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 26 | r1 = rf(orgID, userName) 27 | } else { 28 | r1 = ret.Error(1) 29 | } 30 | 31 | return r0, r1 32 | } 33 | 34 | // AssociateOrgUserByUsername provides a mock function with given fields: orgID, userName 35 | func (_m *PAASClient) AssociateOrgUserByUsername(orgID string, userName string) (*cf.Role, error) { 36 | ret := _m.Called(orgID, userName) 37 | 38 | var r0 *cf.Role 39 | if rf, ok := ret.Get(0).(func(string, string) *cf.Role); ok { 40 | r0 = rf(orgID, userName) 41 | } else { 42 | r0 = ret.Get(0).(*cf.Role) 43 | } 44 | 45 | var r1 error 46 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 47 | r1 = rf(orgID, userName) 48 | } else { 49 | r1 = ret.Error(1) 50 | } 51 | 52 | return r0, r1 53 | } 54 | 55 | // AssociateSpaceAuditorByUsername provides a mock function with given fields: spaceID, userName 56 | func (_m *PAASClient) AssociateSpaceAuditorByUsername(spaceID string, userName string) (*cf.Role, error) { 57 | ret := _m.Called(spaceID, userName) 58 | 59 | var r0 *cf.Role 60 | if rf, ok := ret.Get(0).(func(string, string) *cf.Role); ok { 61 | r0 = rf(spaceID, userName) 62 | } else { 63 | r0 = ret.Get(0).(*cf.Role) 64 | } 65 | 66 | var r1 error 67 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 68 | r1 = rf(spaceID, userName) 69 | } else { 70 | r1 = ret.Error(1) 71 | } 72 | 73 | return r0, r1 74 | } 75 | 76 | // AssociateSpaceDeveloperByUsername provides a mock function with given fields: spaceID, userName 77 | func (_m *PAASClient) AssociateSpaceDeveloperByUsername(spaceID string, userName string) (*cf.Role, error) { 78 | ret := _m.Called(spaceID, userName) 79 | 80 | var r0 *cf.Role 81 | if rf, ok := ret.Get(0).(func(string, string) *cf.Role); ok { 82 | r0 = rf(spaceID, userName) 83 | } else { 84 | r0 = ret.Get(0).(*cf.Role) 85 | } 86 | 87 | var r1 error 88 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 89 | r1 = rf(spaceID, userName) 90 | } else { 91 | r1 = ret.Error(1) 92 | } 93 | 94 | return r0, r1 95 | } 96 | 97 | // CreateUser provides a mock function with given fields: req 98 | func (_m *PAASClient) CreateUser(guid string) (*cf.User, error) { 99 | ret := _m.Called(guid) 100 | 101 | var r0 *cf.User 102 | if rf, ok := ret.Get(0).(func(string) *cf.User); ok { 103 | r0 = rf(guid) 104 | } else { 105 | r0 = ret.Get(0).(*cf.User) 106 | } 107 | 108 | var r1 error 109 | if rf, ok := ret.Get(1).(func(string) error); ok { 110 | r1 = rf(guid) 111 | } else { 112 | r1 = ret.Error(1) 113 | } 114 | 115 | return r0, r1 116 | } 117 | 118 | // DeleteUser provides a mock function with given fields: userID 119 | func (_m *PAASClient) DeleteUser(userID string) error { 120 | ret := _m.Called(userID) 121 | 122 | var r0 error 123 | if rf, ok := ret.Get(0).(func(string) error); ok { 124 | r0 = rf(userID) 125 | } else { 126 | r0 = ret.Error(0) 127 | } 128 | 129 | return r0 130 | } 131 | 132 | // GetSpaceByGuid provides a mock function with given fields: guid 133 | func (_m *PAASClient) GetSpaceByGuid(guid string) (*cf.Space, error) { 134 | ret := _m.Called(guid) 135 | 136 | var r0 *cf.Space 137 | if rf, ok := ret.Get(0).(func(string) *cf.Space); ok { 138 | r0 = rf(guid) 139 | } else { 140 | r0 = ret.Get(0).(*cf.Space) 141 | } 142 | 143 | var r1 error 144 | if rf, ok := ret.Get(1).(func(string) error); ok { 145 | r1 = rf(guid) 146 | } else { 147 | r1 = ret.Error(1) 148 | } 149 | 150 | return r0, r1 151 | } 152 | 153 | // ServiceInstanceByGuid provides a mock function with given fields: guid 154 | func (_m *PAASClient) ServiceInstanceByGuid(guid string) (*cf.ServiceInstance, error) { 155 | ret := _m.Called(guid) 156 | 157 | var r0 *cf.ServiceInstance 158 | if rf, ok := ret.Get(0).(func(string) *cf.ServiceInstance); ok { 159 | r0 = rf(guid) 160 | } else { 161 | r0 = ret.Get(0).(*cf.ServiceInstance) 162 | } 163 | 164 | var r1 error 165 | if rf, ok := ret.Get(1).(func(string) error); ok { 166 | r1 = rf(guid) 167 | } else { 168 | r1 = ret.Error(1) 169 | } 170 | 171 | return r0, r1 172 | } 173 | -------------------------------------------------------------------------------- /password.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "errors" 6 | ) 7 | 8 | const ( 9 | upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 10 | lower = "abcdefghijklmnopqrstuvwxyz" 11 | number = "0123456789" 12 | special = "@%-_+,./:" 13 | chars = upper + lower + number + special 14 | ) 15 | 16 | type PasswordGenerator func(int) string 17 | 18 | type password struct { 19 | Password string 20 | Upper int 21 | Lower int 22 | Number int 23 | Special int 24 | } 25 | 26 | func GenerateSecurePassword(n int) string { 27 | for { 28 | p, err := generatePassword(n) 29 | if err != nil { 30 | continue 31 | } 32 | 33 | err = ValidatePassword(p) 34 | if err == nil { 35 | return p.Password 36 | } 37 | } 38 | } 39 | 40 | func ValidatePassword(p password) error { 41 | if p.Password[0] != '-' && 42 | (p.Upper > 0 || p.Lower > 0 || p.Number > 0 || p.Special > 0) { 43 | return nil 44 | } 45 | return errors.New("Invalid password") 46 | } 47 | 48 | func (p *password) AddChar(idx int) { 49 | p.Password = p.Password + string(chars[idx]) 50 | switch { 51 | case idx < len(upper): 52 | p.Upper = p.Upper + 1 53 | case idx < len(upper)+len(lower): 54 | p.Lower = p.Lower + 1 55 | case idx < len(upper)+len(lower)+len(special): 56 | p.Number = p.Number + 1 57 | default: 58 | p.Special = p.Special + 1 59 | } 60 | } 61 | 62 | func generatePassword(n int) (password, error) { 63 | b, err := randomBytes(n) 64 | if err != nil { 65 | return password{}, err 66 | } 67 | p := password{} 68 | for _, char := range b { 69 | idx := int(char) % len(chars) 70 | p.AddChar(idx) 71 | } 72 | return p, nil 73 | } 74 | 75 | func randomBytes(n int) ([]byte, error) { 76 | b := make([]byte, n) 77 | _, err := rand.Read(b) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return b, nil 82 | } 83 | -------------------------------------------------------------------------------- /password_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("password", func() { 9 | var ( 10 | badPassword password 11 | goodPassword password 12 | ) 13 | 14 | BeforeEach(func() { 15 | badPassword = password{Password: "-badpassword", Upper: 0, Lower: 11, Number: 0, Special: 1} 16 | goodPassword = password{Password: "goodpassword", Upper: 0, Lower: 12, Number: 0, Special: 0} 17 | }) 18 | 19 | Describe("Password generation", func() { 20 | Context("With a leading dash", func() { 21 | It("should generate a new password", func() { 22 | Expect(ValidatePassword(badPassword)).To(MatchError("Invalid password")) 23 | }) 24 | }) 25 | Context("Without a leading dash", func() { 26 | It("should accept the password", func() { 27 | Expect(ValidatePassword(goodPassword)).To(BeNil()) 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | jobs: 3 | - name: test-broker 4 | plan: 5 | - get: pull-request 6 | version: every 7 | trigger: true 8 | - get: broker-src 9 | - get: general-task 10 | - put: pull-request 11 | params: 12 | path: pull-request 13 | status: pending 14 | context: run-tests 15 | - task: run-tests 16 | image: general-task 17 | file: pull-request/run-tests.yml 18 | on_success: 19 | put: pull-request 20 | params: 21 | path: pull-request 22 | status: success 23 | context: run-tests 24 | on_failure: 25 | put: pull-request 26 | params: 27 | path: pull-request 28 | status: failure 29 | context: run-tests 30 | 31 | - name: reconfigure 32 | plan: 33 | - get: broker-src 34 | trigger: true 35 | - set_pipeline: deploy-uaa-credentials-broker 36 | file: broker-src/pipeline.yml 37 | 38 | - name: push-broker-staging 39 | serial_groups: [staging] 40 | serial: true 41 | plan: 42 | - in_parallel: 43 | - get: broker-src 44 | trigger: true 45 | passed: [reconfigure] 46 | - get: pipeline-tasks 47 | - put: broker-deploy-staging 48 | params: 49 | path: broker-src 50 | manifest: broker-src/manifest.yml 51 | environment_variables: 52 | UAA_ADDRESS: ((uaa-address-staging)) 53 | UAA_CLIENT_ID: ((uaa-client-id-staging)) 54 | UAA_CLIENT_SECRET: ((uaa-client-secret-staging)) 55 | UAA_ZONE: ((uaa-zone-staging)) 56 | CF_ADDRESS: ((cf-api-url-staging)) 57 | BROKER_USERNAME: ((broker-username-staging)) 58 | BROKER_PASSWORD: ((broker-password-staging)) 59 | EMAIL_ADDRESS: ((email-address-staging)) 60 | - task: update-broker 61 | file: pipeline-tasks/register-service-broker-and-set-plan-visibility.yml 62 | params: 63 | CF_API_URL: ((cf-api-url-staging)) 64 | CF_USERNAME: ((cf-deploy-username-staging)) 65 | CF_PASSWORD: ((cf-deploy-password-staging)) 66 | CF_ORGANIZATION: ((cf-organization-staging)) 67 | CF_SPACE: ((cf-space-staging)) 68 | BROKER_NAME: uaa-credentials-broker 69 | AUTH_USER: ((broker-username-staging)) 70 | AUTH_PASS: ((broker-password-staging)) 71 | SERVICES: cloud-gov-service-account cloud-gov-identity-provider 72 | on_failure: 73 | put: slack 74 | params: 75 | text: | 76 | :x: FAILED to deploy uaa-credentials-broker on ((cf-api-url-staging)) 77 | <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> 78 | channel: ((slack-failure-channel)) 79 | username: ((slack-username)) 80 | icon_url: ((slack-icon-url)) 81 | on_success: 82 | put: slack 83 | params: 84 | text: | 85 | :white_check_mark: Successfully deployed uaa-credentials-broker on ((cf-api-url-staging)) 86 | <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> 87 | channel: ((slack-success-channel)) 88 | username: ((slack-username)) 89 | icon_url: ((slack-icon-url)) 90 | 91 | - name: acceptance-tests-staging 92 | serial_groups: [staging] 93 | serial: true 94 | plan: 95 | - get: broker-src 96 | passed: [push-broker-staging] 97 | trigger: true 98 | - get: general-task 99 | - task: acceptance-tests-staging 100 | image: general-task 101 | file: broker-src/acceptance-tests.yml 102 | params: 103 | CF_API_URL: ((cf-api-url-staging)) 104 | CF_USERNAME: ((cf-deploy-username-test-staging)) 105 | CF_PASSWORD: ((cf-deploy-password-test-staging)) 106 | CF_ORGANIZATION: ((cf-organization-test-staging)) 107 | CF_SPACE: ((cf-space-test-staging)) 108 | UAA_API_URL: ((uaa-address-staging)) 109 | UAA_CLIENT_ID: ((uaa-client-id-test-staging)) 110 | UAA_CLIENT_SECRET: ((uaa-client-secret-test-staging)) 111 | 112 | - name: push-broker-production 113 | serial_groups: [production] 114 | serial: true 115 | plan: 116 | - in_parallel: 117 | - get: broker-src 118 | passed: [acceptance-tests-staging] 119 | trigger: true 120 | - get: pipeline-tasks 121 | passed: [push-broker-staging] 122 | - put: broker-deploy-production 123 | params: 124 | path: broker-src 125 | manifest: broker-src/manifest.yml 126 | environment_variables: 127 | UAA_ADDRESS: ((uaa-address-production)) 128 | UAA_CLIENT_ID: ((uaa-client-id-production)) 129 | UAA_CLIENT_SECRET: ((uaa-client-secret-production)) 130 | UAA_ZONE: ((uaa-zone-production)) 131 | CF_ADDRESS: ((cf-api-url-production)) 132 | BROKER_USERNAME: ((broker-username-production)) 133 | BROKER_PASSWORD: ((broker-password-production)) 134 | EMAIL_ADDRESS: ((email-address-production)) 135 | - task: update-broker-identity-provider 136 | file: pipeline-tasks/register-service-broker-and-set-plan-visibility.yml 137 | params: 138 | CF_API_URL: ((cf-api-url-production)) 139 | CF_USERNAME: ((cf-deploy-username-production)) 140 | CF_PASSWORD: ((cf-deploy-password-production)) 141 | CF_ORGANIZATION: ((cf-organization-production)) 142 | CF_SPACE: ((cf-space-production)) 143 | BROKER_NAME: uaa-credentials-broker 144 | AUTH_USER: ((broker-username-production)) 145 | AUTH_PASS: ((broker-password-production)) 146 | SERVICES: cloud-gov-identity-provider 147 | - task: update-broker-service-account 148 | file: pipeline-tasks/register-service-broker-and-set-plan-visibility.yml 149 | params: 150 | CF_API_URL: ((cf-api-url-production)) 151 | CF_USERNAME: ((cf-deploy-username-production)) 152 | CF_PASSWORD: ((cf-deploy-password-production)) 153 | CF_ORGANIZATION: ((cf-organization-production)) 154 | CF_SPACE: ((cf-space-production)) 155 | BROKER_NAME: uaa-credentials-broker 156 | AUTH_USER: ((broker-username-production)) 157 | AUTH_PASS: ((broker-password-production)) 158 | SERVICES: cloud-gov-service-account 159 | SERVICE_ORGANIZATION_DENYLIST: ((service-account-blacklist)) 160 | on_failure: 161 | put: slack 162 | params: 163 | text: | 164 | :x: FAILED to deploy uaa-credentials-broker on ((cf-api-url-production)) 165 | <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> 166 | channel: ((slack-failure-channel)) 167 | username: ((slack-username)) 168 | icon_url: ((slack-icon-url)) 169 | on_success: 170 | put: slack 171 | params: 172 | text: | 173 | :white_check_mark: Successfully deployed uaa-credentials-broker on ((cf-api-url-production)) 174 | <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> 175 | channel: ((slack-success-channel)) 176 | username: ((slack-username)) 177 | icon_url: ((slack-icon-url)) 178 | 179 | - name: acceptance-tests-production 180 | serial_groups: [production] 181 | serial: true 182 | plan: 183 | - get: broker-src 184 | passed: [push-broker-production] 185 | trigger: true 186 | - get: general-task 187 | - task: acceptance-tests-production 188 | image: general-task 189 | file: broker-src/acceptance-tests.yml 190 | params: 191 | CF_API_URL: ((cf-api-url-production)) 192 | CF_USERNAME: ((cf-deploy-username-test-production)) 193 | CF_PASSWORD: ((cf-deploy-password-test-production)) 194 | CF_ORGANIZATION: ((cf-organization-test-production)) 195 | CF_SPACE: ((cf-space-test-production)) 196 | UAA_API_URL: ((uaa-address-production)) 197 | UAA_CLIENT_ID: ((uaa-client-id-test-production)) 198 | UAA_CLIENT_SECRET: ((uaa-client-secret-test-production)) 199 | 200 | resources: 201 | - name: broker-src 202 | type: git 203 | source: 204 | uri: https://github.com/cloud-gov/uaa-credentials-broker 205 | branch: main 206 | 207 | - name: pipeline-tasks 208 | type: git 209 | source: 210 | uri: https://github.com/cloud-gov/cg-pipeline-tasks 211 | branch: main 212 | commit_verification_keys: ((cloud-gov-pgp-keys)) 213 | 214 | - name: broker-deploy-staging 215 | type: cf 216 | source: 217 | api: ((cf-api-url-staging)) 218 | username: ((cf-deploy-username-staging)) 219 | password: ((cf-deploy-password-staging)) 220 | organization: ((cf-organization-staging)) 221 | space: ((cf-space-staging)) 222 | 223 | - name: broker-deploy-production 224 | type: cf 225 | source: 226 | api: ((cf-api-url-production)) 227 | username: ((cf-deploy-username-production)) 228 | password: ((cf-deploy-password-production)) 229 | organization: ((cf-organization-production)) 230 | space: ((cf-space-production)) 231 | 232 | - name: slack 233 | type: slack-notification 234 | source: 235 | url: ((slack-webhook-url)) 236 | 237 | - name: pull-request 238 | type: pull-request 239 | check_every: 1m 240 | source: 241 | repository: cloud-gov/uaa-credentials-broker 242 | access_token: ((status-access-token)) 243 | disable_forks: true 244 | 245 | - name: general-task 246 | type: registry-image 247 | source: 248 | aws_access_key_id: ((ecr_aws_key)) 249 | aws_secret_access_key: ((ecr_aws_secret)) 250 | repository: general-task 251 | aws_region: us-gov-west-1 252 | tag: latest 253 | 254 | resource_types: 255 | - name: slack-notification 256 | type: registry-image 257 | source: 258 | aws_access_key_id: ((ecr_aws_key)) 259 | aws_secret_access_key: ((ecr_aws_secret)) 260 | repository: slack-notification-resource 261 | aws_region: us-gov-west-1 262 | tag: latest 263 | 264 | - name: git 265 | type: registry-image 266 | source: 267 | aws_access_key_id: ((ecr_aws_key)) 268 | aws_secret_access_key: ((ecr_aws_secret)) 269 | repository: git-resource 270 | aws_region: us-gov-west-1 271 | tag: latest 272 | 273 | - name: cf 274 | type: registry-image 275 | source: 276 | aws_access_key_id: ((ecr_aws_key)) 277 | aws_secret_access_key: ((ecr_aws_secret)) 278 | repository: cf-resource 279 | aws_region: us-gov-west-1 280 | tag: latest 281 | 282 | - name: registry-image 283 | type: registry-image 284 | source: 285 | aws_access_key_id: ((ecr_aws_key)) 286 | aws_secret_access_key: ((ecr_aws_secret)) 287 | repository: registry-image-resource 288 | aws_region: us-gov-west-1 289 | tag: latest 290 | 291 | - name: pull-request 292 | type: registry-image 293 | source: 294 | aws_access_key_id: ((ecr_aws_key)) 295 | aws_secret_access_key: ((ecr_aws_secret)) 296 | repository: github-pr-resource 297 | aws_region: us-gov-west-1 298 | tag: latest 299 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -x 4 | 5 | go install github.com/onsi/ginkgo/ginkgo@latest 6 | 7 | pushd uaa-credentials-broker 8 | 9 | go get -v -d ./... 10 | 11 | go test -v ./... 12 | 13 | popd 14 | -------------------------------------------------------------------------------- /run-tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | inputs: 5 | - name: pull-request 6 | path: uaa-credentials-broker 7 | 8 | run: 9 | path: uaa-credentials-broker/run-tests.sh 10 | -------------------------------------------------------------------------------- /uaa.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | 8 | "code.cloudfoundry.org/lager" 9 | ) 10 | 11 | type Users struct { 12 | Resources []User 13 | TotalResults int 14 | } 15 | 16 | type User struct { 17 | ID string `json:"id,omitempty"` 18 | UserName string `json:"userName,omitempty"` 19 | Password string `json:"password,omitempty"` 20 | Active bool `json:"active,omitempty"` 21 | Emails []Email `json:"emails"` 22 | } 23 | 24 | type Clients struct { 25 | Resources []Client 26 | TotalResults int 27 | } 28 | 29 | type Client struct { 30 | ID string `json:"client_id,omitempty"` 31 | ClientSecret string `json:"client_secret,omitempty"` 32 | Name string `json:"name,omitempty"` 33 | AuthorizedGrantTypes []string `json:"authorized_grant_types,omitempty"` 34 | Scope []string `json:"scope,omitempty"` 35 | RedirectURI []string `json:"redirect_uri,omitempty"` 36 | Active bool `json:"active,omitempty"` 37 | AccessTokenValidity int `json:"access_token_validity,omitempty"` 38 | RefreshTokenValidity int `json:"refresh_token_validity,omitempty"` 39 | AllowPublic bool `json:"allowpublic,omitempty"` 40 | } 41 | 42 | type Email struct { 43 | Value string `json:"value,omitempty"` 44 | Primary bool `json:"primary"` 45 | } 46 | 47 | type AuthClient interface { 48 | CreateClient(client Client) (Client, error) 49 | DeleteClient(clientID string) error 50 | GetUser(userID string) (User, error) 51 | CreateUser(user User) (User, error) 52 | DeleteUser(userID string) error 53 | } 54 | 55 | type UAAClient struct { 56 | logger lager.Logger 57 | client *http.Client 58 | endpoint string 59 | zone string 60 | } 61 | 62 | func (c *UAAClient) CreateClient(client Client) (Client, error) { 63 | c.logger.Info("uaa-create-client", lager.Data{"clientID": client.ID}) 64 | 65 | body, _ := encodeBody(client) 66 | req, _ := http.NewRequest("POST", fmt.Sprintf("%s/oauth/clients", c.endpoint), body) 67 | req.Header.Add("X-Identity-Zone-Id", c.zone) 68 | req.Header.Add("Content-Type", "application/json") 69 | req.Header.Add("Accept", "application/json") 70 | resp, err := c.client.Do(req) 71 | if err != nil { 72 | return Client{}, err 73 | } 74 | 75 | if resp.StatusCode != 201 { 76 | output := map[string]any{} 77 | err = decodeBody(resp.Body, &output) 78 | if err != nil { 79 | return Client{}, err 80 | } 81 | return Client{}, fmt.Errorf("expected status 201; got: %d. error: %s", resp.StatusCode, output) 82 | } 83 | 84 | err = decodeBody(resp.Body, &client) 85 | if err != nil { 86 | return Client{}, err 87 | } 88 | 89 | return client, nil 90 | } 91 | 92 | func (c *UAAClient) DeleteClient(clientID string) error { 93 | c.logger.Info("uaa-delete-client", lager.Data{"clientID": clientID}) 94 | 95 | req, _ := http.NewRequest("DELETE", fmt.Sprintf("%s/oauth/clients/%s", c.endpoint, clientID), nil) 96 | req.Header.Add("X-Identity-Zone-Id", c.zone) 97 | req.Header.Add("Content-Type", "application/json") 98 | req.Header.Add("Accept", "application/json") 99 | resp, err := c.client.Do(req) 100 | if err != nil { 101 | return err 102 | } 103 | if resp.StatusCode != 200 { 104 | return fmt.Errorf("Expected status 200; got: %d", resp.StatusCode) 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (c *UAAClient) GetUser(userID string) (User, error) { 111 | c.logger.Info("uaa-get-user", lager.Data{"userID": userID}) 112 | 113 | u, _ := url.Parse(fmt.Sprintf("%s/Users", c.endpoint)) 114 | q := u.Query() 115 | q.Add("filter", fmt.Sprintf(`userName eq "%s"`, userID)) 116 | q.Add("count", "1") 117 | u.RawQuery = q.Encode() 118 | 119 | req, _ := http.NewRequest("GET", u.String(), nil) 120 | req.Header.Add("X-Identity-Zone-Id", c.zone) 121 | req.Header.Add("Content-Type", "application/json") 122 | req.Header.Add("Accept", "application/json") 123 | resp, err := c.client.Do(req) 124 | if err != nil { 125 | return User{}, err 126 | } 127 | 128 | users := Users{} 129 | err = decodeBody(resp.Body, &users) 130 | if err != nil { 131 | return User{}, err 132 | } 133 | 134 | if users.TotalResults != 1 { 135 | return User{}, fmt.Errorf("Expected to find exactly one user; got %d", users.TotalResults) 136 | } 137 | 138 | return users.Resources[0], nil 139 | } 140 | 141 | func (c *UAAClient) CreateUser(user User) (User, error) { 142 | c.logger.Info("uaa-create-user", lager.Data{"userID": user.UserName}) 143 | 144 | body, _ := encodeBody(user) 145 | req, _ := http.NewRequest("POST", fmt.Sprintf("%s/Users", c.endpoint), body) 146 | req.Header.Add("X-Identity-Zone-Id", c.zone) 147 | req.Header.Add("Content-Type", "application/json") 148 | req.Header.Add("Accept", "application/json") 149 | resp, err := c.client.Do(req) 150 | if err != nil { 151 | return User{}, err 152 | } 153 | 154 | if resp.StatusCode != 201 { 155 | return User{}, fmt.Errorf("Expected status 201; got: %d", resp.StatusCode) 156 | } 157 | 158 | err = decodeBody(resp.Body, &user) 159 | if err != nil { 160 | return User{}, err 161 | } 162 | 163 | return user, nil 164 | } 165 | 166 | func (c *UAAClient) DeleteUser(userID string) error { 167 | c.logger.Info("uaa-delete-user", lager.Data{"userID": userID}) 168 | 169 | req, _ := http.NewRequest("DELETE", fmt.Sprintf("%s/Users/%s", c.endpoint, userID), nil) 170 | req.Header.Add("X-Identity-Zone-Id", c.zone) 171 | req.Header.Add("Content-Type", "application/json") 172 | req.Header.Add("Accept", "application/json") 173 | resp, err := c.client.Do(req) 174 | if err != nil { 175 | return err 176 | } 177 | if resp.StatusCode != 200 { 178 | return fmt.Errorf("Expected status 200; got: %d", resp.StatusCode) 179 | } 180 | 181 | return nil 182 | } 183 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | ) 8 | 9 | func encodeBody(obj interface{}) (io.Reader, error) { 10 | buffer := bytes.NewBuffer(nil) 11 | if err := json.NewEncoder(buffer).Encode(obj); err != nil { 12 | return nil, err 13 | } 14 | return buffer, nil 15 | } 16 | 17 | func decodeBody(body io.ReadCloser, out interface{}) error { 18 | defer body.Close() 19 | return json.NewDecoder(body).Decode(out) 20 | } 21 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestDecodeBody(t *testing.T) { 11 | body := `{"foo":"bar"}` 12 | output := map[string]any{} 13 | req, err := http.NewRequest("post", "fake-url", strings.NewReader(body)) 14 | if err != nil { 15 | t.Error(err) 16 | } 17 | 18 | err = decodeBody(req.Body, &output) 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | 23 | if !reflect.DeepEqual(output, map[string]any{"foo": "bar"}) { 24 | t.Error("output does not equal expected value") 25 | } 26 | } 27 | --------------------------------------------------------------------------------