├── .envrc ├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── bin └── test ├── cli ├── auth_callback_server.go ├── auth_callback_server_test.go ├── authcode_client_impersonator.go ├── authcode_client_impersonator_test.go ├── cli_suite_test.go ├── errors.go ├── implicit_client_impersonator.go ├── implicit_client_impersonator_test.go ├── interactive_prompt.go ├── interactive_prompt_test.go ├── logger.go ├── printer.go └── printer_test.go ├── cmd ├── activate_user.go ├── activate_user_test.go ├── add_member.go ├── add_member_test.go ├── api_client.go ├── cmd_suite_test.go ├── context.go ├── context_test.go ├── contexts.go ├── contexts_test.go ├── create_client.go ├── create_client_test.go ├── create_group.go ├── create_group_test.go ├── create_user.go ├── create_user_test.go ├── curl.go ├── curl_test.go ├── deactivate_user.go ├── deactivate_user_test.go ├── delete_client.go ├── delete_client_test.go ├── delete_user.go ├── delete_user_test.go ├── get_authcode_token.go ├── get_authcode_token_test.go ├── get_client.go ├── get_client_credentials_token.go ├── get_client_credentials_token_test.go ├── get_client_test.go ├── get_group.go ├── get_group_test.go ├── get_implicit_token.go ├── get_implicit_token_test.go ├── get_password_token.go ├── get_password_token_test.go ├── get_token_key.go ├── get_token_key_test.go ├── get_token_keys.go ├── get_token_keys_test.go ├── get_user.go ├── get_user_test.go ├── group_mapping_validations.go ├── group_mapping_validations_test.go ├── helpers_test.go ├── info.go ├── info_test.go ├── list_clients.go ├── list_clients_test.go ├── list_group_mappings.go ├── list_group_mappings_test.go ├── list_groups.go ├── list_groups_test.go ├── list_users.go ├── list_users_test.go ├── map_group.go ├── map_group_test.go ├── refresh_token.go ├── refresh_token_test.go ├── remove_member.go ├── remove_member_test.go ├── root.go ├── set_client_secret.go ├── set_client_secret_test.go ├── target.go ├── target_test.go ├── unmap_group.go ├── unmap_group_test.go ├── update_client.go ├── update_client_test.go ├── userinfo.go ├── userinfo_test.go ├── validations.go └── version.go ├── config ├── config.go ├── config_suite_test.go ├── config_test.go ├── config_unix.go ├── config_unix_test.go ├── config_win.go └── config_win_test.go ├── fixtures ├── builders.go ├── group_mappings_fixtures.go ├── groups.go └── users.go ├── go.mod ├── go.sum ├── help ├── client_credentials.go ├── clients.go ├── context.go ├── implicit_grant.go ├── list_users.go ├── password_grant.go ├── refresh_token.go ├── root.go └── userinfo.go ├── main.go ├── utils ├── arrayify.go ├── arrayify_test.go ├── boolean_pointers.go ├── contains.go ├── contains_test.go ├── stringifier.go ├── stringifier_test.go ├── styling.go ├── url_helpers.go ├── url_helpers_test.go └── utils_suite_test.go └── version └── version.go /.envrc: -------------------------------------------------------------------------------- 1 | export GO111MODULE=on 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.xml 2 | .idea/ 3 | .vscode/ 4 | uaa-cli 5 | build/ 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build ci clean format ginkgo test install 2 | 3 | BUILD_DEST = build/uaa 4 | INSTALL_DEST = $(GOPATH)/bin/uaa 5 | COMMIT_HASH=$(shell git rev-parse --short HEAD) 6 | GOFILES=$(shell find . -type f -name '*.go') 7 | 8 | ifndef VERSION 9 | VERSION = DEV 10 | endif 11 | 12 | GOFLAGS := -v -ldflags "-X code.cloudfoundry.org/uaa-cli/version.Version=${VERSION} -X code.cloudfoundry.org/uaa-cli/version.Commit=${COMMIT_HASH}" 13 | 14 | all: test clean build 15 | 16 | clean: 17 | rm -rf build 18 | 19 | format: 20 | go fmt ./... 21 | 22 | ginkgo: 23 | go run github.com/onsi/ginkgo/v2/ginkgo -v -r --randomize-suites --randomize-all -race 24 | 25 | test: format ginkgo 26 | 27 | ci: ginkgo 28 | 29 | build: 30 | mkdir -p build 31 | CGO_ENABLED=0 go build $(GOFLAGS) -o $(BUILD_DEST) 32 | 33 | build_all: 34 | mkdir -p build 35 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build $(GOFLAGS) -o $(BUILD_DEST)-darwin-arm64 36 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(GOFLAGS) -o $(BUILD_DEST)-darwin-amd64 37 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(GOFLAGS) -o $(BUILD_DEST)-linux-amd64 38 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build $(GOFLAGS) -o $(BUILD_DEST)-windows-amd64 39 | 40 | install: 41 | rm -rf $(INSTALL_DEST) 42 | cp $(BUILD_DEST) $(INSTALL_DEST) 43 | 44 | cve: build 45 | grype file:$(BUILD_DEST) -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | uaa-cli 2 | 3 | Copyright (c) 2017-Present CloudFoundry.org Foundation, Inc. All Rights 4 | Reserved. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UAA Command Line Interface 2 | 3 | CLI for [UAA](https://github.com/cloudfoundry/uaa) written in golang. This is an alterntive to using uaac which is wirtten in Ruby. At this time it performs a limited subset of the features provided by the [uaac](https://github.com/cloudfoundry/cf-uaac) gem. The team plans to continue development on the golang CLI going forward, and once it's considered fully GA, intends to place it alongside uaac with a long-term intention of one day deprecating uaac. 4 | 5 | ### Goals 6 | 7 | - To provide a CLI which can be easily installed in environments without a functioning Ruby setup 8 | - To more closely conform to the style of other widely used CLIs in the CF ecosystem, e.g. the cf CLI. Commands should be of the form VERB-NOUN, similar to `cf delete-app`. 9 | - To provide outputs that are machine-parseable whenever possible. 10 | - To improve the quality of help strings and error messages so that users can self-diagnose problems and unblock themselves. 11 | - To provide only the essential, highly used and/or required command options. 12 | 13 | ### Trying out the latest code 14 | 15 | ``` 16 | go get code.cloudfoundry.org/uaa-cli 17 | cd $GOPATH/src/code.cloudfoundry.org/uaa-cli 18 | make && make install 19 | uaa -h 20 | ``` 21 | Or, install it using brew. It's been made available as part of the [cloudfoundry tap](https://github.com/cloudfoundry/homebrew-tap) 22 | 23 | ``` 24 | brew install cloudfoundry/tap/uaa-cli 25 | ``` 26 | 27 | ## Development notes 28 | 29 | ### Setting up Go 30 | 31 | If you don't have a working Go setup 32 | 33 | ``` 34 | brew update 35 | brew install go 36 | 37 | echo 'export GOPATH="$HOME/go"' >> ~/.bash_profile 38 | echo 'export PATH="$GOPATH/bin:$PATH"' >> ~/.bash_profile 39 | ``` 40 | 41 | ### Running the tests 42 | 43 | ``` 44 | cd $GOPATH/src/code.cloudfoundry.org/uaa-cli 45 | ginkgo -r -randomizeAllSpecs -randomizeSuites 46 | ``` 47 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | go run github.com/onsi/ginkgo/v2/ginkgo -v -r --randomize-suites --randomize-all -race $@ 6 | -------------------------------------------------------------------------------- /cli/auth_callback_server.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | type CallbackServer interface { 12 | Html() string 13 | CSS() string 14 | Javascript() string 15 | Port() int 16 | Log() Logger 17 | Hangup(chan url.Values, url.Values) 18 | Start(chan url.Values) 19 | } 20 | 21 | type AuthCallbackServer struct { 22 | html string 23 | css string 24 | javascript string 25 | port int 26 | log Logger 27 | hangupFunc func(chan url.Values, url.Values) 28 | } 29 | 30 | func NewAuthCallbackServer(html, css, js string, log Logger, port int) AuthCallbackServer { 31 | acs := AuthCallbackServer{html: html, css: css, javascript: js, log: log, port: port} 32 | acs.SetHangupFunc(func(done chan url.Values, vals url.Values) {}) 33 | return acs 34 | } 35 | 36 | func (acs AuthCallbackServer) Html() string { 37 | return acs.html 38 | } 39 | func (acs AuthCallbackServer) CSS() string { 40 | return acs.css 41 | } 42 | func (acs AuthCallbackServer) Javascript() string { 43 | return acs.javascript 44 | } 45 | func (acs AuthCallbackServer) Port() int { 46 | return acs.port 47 | } 48 | func (acs AuthCallbackServer) Log() Logger { 49 | return acs.log 50 | } 51 | func (acs AuthCallbackServer) Hangup(done chan url.Values, values url.Values) { 52 | acs.hangupFunc(done, values) 53 | } 54 | func (acs *AuthCallbackServer) SetHangupFunc(hangupFunc func(chan url.Values, url.Values)) { 55 | acs.hangupFunc = hangupFunc 56 | } 57 | 58 | func (acs AuthCallbackServer) Start(done chan url.Values) { 59 | callbackValues := make(chan url.Values) 60 | serveMux := http.NewServeMux() 61 | srv := &http.Server{ 62 | Addr: fmt.Sprintf(":%v", acs.port), 63 | Handler: serveMux, 64 | } 65 | 66 | go func() { 67 | value := <-callbackValues 68 | close(callbackValues) 69 | srv.Close() 70 | done <- value 71 | }() 72 | 73 | attemptHangup := func(queryParams url.Values) { 74 | time.Sleep(10 * time.Millisecond) 75 | acs.Hangup(callbackValues, queryParams) 76 | } 77 | 78 | serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 79 | io.WriteString(w, acs.css) 80 | io.WriteString(w, acs.html) 81 | io.WriteString(w, acs.javascript) 82 | acs.log.Infof("Local server received request to %v %v", r.Method, r.RequestURI) 83 | 84 | // This is a goroutine because we want this handleFunc to complete before 85 | // Server.Close is invoked by listeners on the callbackValues channel. 86 | go attemptHangup(r.URL.Query()) 87 | }) 88 | 89 | go func() { 90 | acs.log.Infof("Starting local HTTP server on port %v", acs.port) 91 | acs.log.Info("Waiting for authorization redirect from UAA...") 92 | if err := srv.ListenAndServe(); err != nil { 93 | acs.log.Infof("Stopping local HTTP server on port %v", acs.port) 94 | } 95 | }() 96 | } 97 | 98 | type FakeCallbackServer struct { 99 | html string 100 | css string 101 | javascript string 102 | port int 103 | log Logger 104 | hangupFunc func(chan url.Values, url.Values) 105 | } 106 | 107 | func (fcs FakeCallbackServer) Html() string { 108 | return fcs.html 109 | } 110 | func (fcs FakeCallbackServer) CSS() string { 111 | return fcs.css 112 | } 113 | func (fcs FakeCallbackServer) Javascript() string { 114 | return fcs.javascript 115 | } 116 | func (fcs FakeCallbackServer) Port() int { 117 | return fcs.port 118 | } 119 | func (fcs FakeCallbackServer) Log() Logger { 120 | return fcs.log 121 | } 122 | func (fcs FakeCallbackServer) Hangup(done chan url.Values, values url.Values) { 123 | fcs.hangupFunc(done, values) 124 | } 125 | func (fcs *FakeCallbackServer) SetHangupFunc(hangupFunc func(chan url.Values, url.Values)) { 126 | fcs.hangupFunc = hangupFunc 127 | } 128 | func (fcs FakeCallbackServer) Start(done chan url.Values) { 129 | values := url.Values{} 130 | values.Add("access_token", "a_fake_token") 131 | values.Add("expires_in", "4000") 132 | values.Add("token_type", "bearer") 133 | values.Add("scope", "openid") 134 | values.Add("jti", "jti_value") 135 | 136 | done <- values 137 | } 138 | -------------------------------------------------------------------------------- /cli/auth_callback_server_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | . "code.cloudfoundry.org/uaa-cli/cli" 5 | "fmt" 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | "github.com/onsi/gomega/gstruct" 9 | "io/ioutil" 10 | "math/rand" 11 | "net/http" 12 | "net/url" 13 | "time" 14 | ) 15 | 16 | func serverUrl(port int) string { 17 | return fmt.Sprintf("http://localhost:%v/", port) 18 | } 19 | 20 | var _ = Describe("AuthCallbackServer", func() { 21 | var ( 22 | httpClient *http.Client 23 | acs AuthCallbackServer 24 | done chan url.Values 25 | randPort int 26 | logger Logger 27 | ) 28 | 29 | BeforeEach(func() { 30 | randPort = rand.Intn(50000-8000) + 8000 31 | 32 | httpClient = &http.Client{} 33 | logger = NewLogger(ioutil.Discard, ioutil.Discard, ioutil.Discard, ioutil.Discard) 34 | acs = NewAuthCallbackServer( 35 | "

Hello There

", 36 | "", 37 | "", 38 | logger, 39 | randPort) 40 | 41 | done = make(chan url.Values) 42 | }) 43 | 44 | AfterEach(func() { 45 | close(done) 46 | }) 47 | 48 | Describe("Start", func() { 49 | It("serves static content on the configured port", func() { 50 | acs.SetHangupFunc(func(donedone chan url.Values, values url.Values) { 51 | donedone <- url.Values{} // exit immediately after first call 52 | }) 53 | 54 | acs.Start(done) 55 | 56 | var err error 57 | var resp *http.Response 58 | Eventually(func() (*http.Response, error) { 59 | resp, err = httpClient.Get(serverUrl(randPort)) 60 | return resp, err 61 | }, AuthCallbackTimeout, AuthCallbackPollInterval).Should(gstruct.PointTo(gstruct.MatchFields( 62 | gstruct.IgnoreExtras, gstruct.Fields{ 63 | "StatusCode": Equal(200), 64 | "Body": Not(BeNil()), 65 | }, 66 | ))) 67 | 68 | Eventually(done).Should(Receive()) 69 | 70 | parsedBody, err := ioutil.ReadAll(resp.Body) 71 | Expect(err).NotTo(HaveOccurred()) 72 | Expect(string(parsedBody)).To(ContainSubstring("Hello There")) 73 | Expect(string(parsedBody)).To(ContainSubstring("background: #F00")) 74 | Expect(string(parsedBody)).To(ContainSubstring("Objective judgement")) 75 | }) 76 | }) 77 | 78 | It("uses the Hangup func to decide when to close the server", func() { 79 | acs.SetHangupFunc(func(donedone chan url.Values, values url.Values) { 80 | if values.Get("code") != "" { 81 | donedone <- values 82 | } 83 | }) 84 | 85 | acs.Start(done) 86 | 87 | Eventually(func() (*http.Response, error) { 88 | return httpClient.Get(serverUrl(randPort)) 89 | }, AuthCallbackTimeout, AuthCallbackPollInterval).Should(gstruct.PointTo(gstruct.MatchFields( 90 | gstruct.IgnoreExtras, gstruct.Fields{ 91 | "StatusCode": Equal(200), 92 | "Body": Not(BeNil()), 93 | }, 94 | ))) 95 | Eventually(func() (*http.Response, error) { 96 | return httpClient.Get(serverUrl(randPort) + "?foo=not_the_code") 97 | }, AuthCallbackTimeout, AuthCallbackPollInterval).Should(gstruct.PointTo(gstruct.MatchFields( 98 | gstruct.IgnoreExtras, gstruct.Fields{ 99 | "StatusCode": Equal(200), 100 | "Body": Not(BeNil()), 101 | }, 102 | ))) 103 | Eventually(func() (*http.Response, error) { 104 | return httpClient.Get(serverUrl(randPort) + "?code=secret_code") 105 | }, AuthCallbackTimeout, AuthCallbackPollInterval).Should(gstruct.PointTo(gstruct.MatchFields( 106 | gstruct.IgnoreExtras, gstruct.Fields{ 107 | "StatusCode": Equal(200), 108 | "Body": Not(BeNil()), 109 | }, 110 | ))) 111 | 112 | // Server should close after first request with "code" param 113 | // Sleep so we don't call while the server is still closing 114 | time.Sleep(20 * time.Millisecond) 115 | _, err := httpClient.Get(serverUrl(randPort)) 116 | Expect(err).To(HaveOccurred()) 117 | 118 | var requestParams url.Values 119 | Eventually(done).Should(Receive(&requestParams)) 120 | Expect(requestParams.Get("code")).To(Equal("secret_code")) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /cli/authcode_client_impersonator.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "code.cloudfoundry.org/uaa-cli/config" 10 | "code.cloudfoundry.org/uaa-cli/utils" 11 | "github.com/cloudfoundry-community/go-uaa" 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | type AuthcodeClientImpersonator struct { 16 | config config.Config 17 | ClientID string 18 | ClientSecret string 19 | TokenFormat string 20 | Scope string 21 | UaaBaseURL string 22 | Port int 23 | Log Logger 24 | AuthCallbackServer CallbackServer 25 | BrowserLauncher func(string) error 26 | done chan oauth2.Token 27 | } 28 | 29 | const authcodeCallbackHTML = ` 30 |

Authorization Code Grant: Success

31 |

The UAA redirected you to this page with an authorization code.

32 |

The CLI will exchange this code for an access token. You may close this window.

33 | ` 34 | 35 | func NewAuthcodeClientImpersonator( 36 | config config.Config, 37 | clientId, 38 | clientSecret, 39 | tokenFormat, 40 | scope string, 41 | port int, 42 | log Logger, 43 | launcher func(string) error) AuthcodeClientImpersonator { 44 | 45 | impersonator := AuthcodeClientImpersonator{ 46 | config: config, 47 | ClientID: clientId, 48 | ClientSecret: clientSecret, 49 | TokenFormat: tokenFormat, 50 | Scope: scope, 51 | Port: port, 52 | BrowserLauncher: launcher, 53 | Log: log, 54 | done: make(chan oauth2.Token), 55 | } 56 | 57 | callbackServer := NewAuthCallbackServer(authcodeCallbackHTML, CallbackCSS, "", log, port) 58 | callbackServer.SetHangupFunc(func(done chan url.Values, values url.Values) { 59 | token := values.Get("code") 60 | if token != "" { 61 | done <- values 62 | } 63 | }) 64 | impersonator.AuthCallbackServer = callbackServer 65 | 66 | return impersonator 67 | } 68 | 69 | func (aci AuthcodeClientImpersonator) Start() { 70 | go func() { 71 | urlValues := make(chan url.Values) 72 | go aci.AuthCallbackServer.Start(urlValues) 73 | values := <-urlValues 74 | code := values.Get("code") 75 | 76 | tokenFormat := uaa.JSONWebToken //TODO: Use aci tokenformat to convert from string to int 77 | 78 | redirectUrl, err := url.ParseRequestURI(aci.redirectUri()) 79 | NotifyErrorsWithRetry(err, aci.Log, aci.config) 80 | 81 | api, err := uaa.New( 82 | aci.config.GetActiveTarget().BaseUrl, 83 | uaa.WithAuthorizationCode( 84 | aci.ClientID, 85 | aci.ClientSecret, 86 | code, 87 | tokenFormat, 88 | redirectUrl, 89 | ), 90 | uaa.WithZoneID(aci.config.ZoneSubdomain), 91 | uaa.WithSkipSSLValidation(aci.config.GetActiveTarget().SkipSSLValidation), 92 | uaa.WithVerbosity(aci.config.Verbose), 93 | ) 94 | NotifyErrorsWithRetry(err, aci.Log, aci.config) 95 | token, err := api.Token(context.Background()) 96 | NotifyErrorsWithRetry(err, aci.Log, aci.config) 97 | 98 | aci.Done() <- *token 99 | }() 100 | } 101 | func (aci AuthcodeClientImpersonator) Authorize() { 102 | requestValues := url.Values{} 103 | requestValues.Add("response_type", "code") 104 | requestValues.Add("client_id", aci.ClientID) 105 | requestValues.Add("scope", aci.Scope) 106 | requestValues.Add("redirect_uri", aci.redirectUri()) 107 | 108 | authUrl, err := utils.BuildUrl(aci.config.GetActiveTarget().BaseUrl, "/oauth/authorize") 109 | if err != nil { 110 | aci.Log.Error("Something went wrong while building the authorization URL.") 111 | os.Exit(1) 112 | } 113 | authUrl.RawQuery = requestValues.Encode() 114 | 115 | aci.Log.Info("Launching browser window to " + authUrl.String() + " where the user should login and grant approvals") 116 | aci.BrowserLauncher(authUrl.String()) 117 | } 118 | func (aci AuthcodeClientImpersonator) Done() chan oauth2.Token { 119 | return aci.done 120 | } 121 | func (aci AuthcodeClientImpersonator) redirectUri() string { 122 | return fmt.Sprintf("http://localhost:%v", aci.Port) 123 | } 124 | -------------------------------------------------------------------------------- /cli/authcode_client_impersonator_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | . "code.cloudfoundry.org/uaa-cli/cli" 5 | 6 | "net/http" 7 | "net/url" 8 | 9 | "time" 10 | 11 | cliConfig "code.cloudfoundry.org/uaa-cli/config" 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | . "github.com/onsi/gomega/ghttp" 15 | "github.com/onsi/gomega/gstruct" 16 | ) 17 | 18 | var _ = Describe("AuthcodeClientImpersonator", func() { 19 | var ( 20 | impersonator AuthcodeClientImpersonator 21 | logger Logger 22 | httpClient *http.Client 23 | config cliConfig.Config 24 | launcher TestLauncher 25 | uaaServer *Server 26 | ) 27 | 28 | BeforeEach(func() { 29 | httpClient = &http.Client{} 30 | launcher = TestLauncher{} 31 | uaaServer = NewServer() 32 | config = cliConfig.NewConfigWithServerURL(uaaServer.URL()) 33 | logger = NewLogger(GinkgoWriter, GinkgoWriter, GinkgoWriter, GinkgoWriter) 34 | }) 35 | 36 | Describe("NewAuthcodeClientImpersonator", func() { 37 | BeforeEach(func() { 38 | launcher := TestLauncher{} 39 | impersonator = NewAuthcodeClientImpersonator(config, "authcodeClientId", "authcodesecret", "jwt", "openid", 9090, logger, launcher.Run) 40 | }) 41 | 42 | Describe("configures an AuthCallbackListener", func() { 43 | It("with appropriate static content", func() { 44 | Expect(impersonator.AuthCallbackServer.CSS()).To(ContainSubstring("Source Sans Pro")) 45 | Expect(impersonator.AuthCallbackServer.Html()).To(ContainSubstring("Authorization Code Grant: Success")) 46 | }) 47 | 48 | It("with the desired port", func() { 49 | Expect(impersonator.AuthCallbackServer.Port()).To(Equal(9090)) 50 | }) 51 | 52 | It("with its logger", func() { 53 | Expect(impersonator.AuthCallbackServer.Log()).NotTo(Equal(Logger{})) 54 | Expect(impersonator.AuthCallbackServer.Log()).To(Equal(logger)) 55 | }) 56 | 57 | It("with hangup func that looks for code in query params", func() { 58 | done := make(chan url.Values) 59 | 60 | urlParams := url.Values{} 61 | urlParams.Add("code", "56575db17b164e568668c0085ed14ae1") 62 | go impersonator.AuthCallbackServer.Hangup(done, urlParams) 63 | 64 | Expect(<-done).To(Equal(urlParams)) 65 | }) 66 | }) 67 | }) 68 | 69 | Describe("#Start", func() { 70 | It("starts the AuthCallbackServer", func() { 71 | contentTypeJson := http.Header{} 72 | contentTypeJson.Add("Content-Type", "application/json") 73 | 74 | uaaServer.RouteToHandler("POST", "/oauth/token", CombineHandlers( 75 | RespondWith(http.StatusOK, `{ 76 | "access_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ", 77 | "token_type" : "bearer", 78 | "expires_in" : 3000, 79 | "scope" : "openid", 80 | "jti" : "bc4885d950854fed9a938e96b13ca519" 81 | }`, contentTypeJson), 82 | VerifyRequest("POST", "/oauth/token"), 83 | VerifyHeader(http.Header{"Authorization": []string{"Basic YXV0aGNvZGVJZDphdXRoY29kZXNlY3JldA=="}}), 84 | VerifyBody([]byte(`code=secretcode&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A9090&response_type=token&token_format=jwt`)), 85 | ), 86 | ) 87 | 88 | impersonator = NewAuthcodeClientImpersonator(config, "authcodeId", "authcodesecret", "jwt", "openid", 9090, logger, launcher.Run) 89 | 90 | // Start the callback server 91 | go impersonator.Start() 92 | 93 | // Hit the callback server with an authcode 94 | Eventually(func() (*http.Response, error) { 95 | return httpClient.Get("http://localhost:9090/?code=secretcode") 96 | }, AuthCallbackTimeout, AuthCallbackPollInterval).Should(gstruct.PointTo(gstruct.MatchFields( 97 | gstruct.IgnoreExtras, gstruct.Fields{ 98 | "StatusCode": Equal(200), 99 | "Body": Not(BeNil()), 100 | }, 101 | ))) 102 | 103 | // The callback server should have exchanged the code for a token 104 | tokenResponse := <-impersonator.Done() 105 | Expect(tokenResponse.AccessToken).To(Equal("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 106 | Expect(tokenResponse.TokenType).To(Equal("bearer")) 107 | Expect(tokenResponse.Extra("scope")).To(Equal("openid")) 108 | Expect(tokenResponse.Extra("jti")).To(Equal("bc4885d950854fed9a938e96b13ca519")) 109 | Expect(tokenResponse.Expiry).Should(BeTemporally("~", time.Now(), 3000*time.Second)) 110 | }) 111 | }) 112 | 113 | Describe("#Authorize", func() { 114 | It("launches a browser to the authorize page", func() { 115 | impersonator = NewAuthcodeClientImpersonator(config, "authcodeId", "authcodesecret", "jwt", "openid", 9090, logger, launcher.Run) 116 | 117 | impersonator.Authorize() 118 | 119 | Expect(launcher.TargetUrl).To(Equal(uaaServer.URL() + "/oauth/authorize?client_id=authcodeId&redirect_uri=http%3A%2F%2Flocalhost%3A9090&response_type=code&scope=openid")) 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /cli/cli_suite_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | var ( 11 | AuthCallbackTimeout float64 = 5 12 | AuthCallbackPollInterval float64 = 0.01 13 | ) 14 | 15 | func TestCli(t *testing.T) { 16 | RegisterFailHandler(Fail) 17 | RunSpecs(t, "Cli Suite") 18 | } 19 | -------------------------------------------------------------------------------- /cli/errors.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "code.cloudfoundry.org/uaa-cli/config" 9 | "github.com/cloudfoundry-community/go-uaa" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | const MISSING_TARGET = "You must set a target in order to use this command." 14 | const MISSING_CONTEXT = "You must have a token in your context to perform this command." 15 | 16 | func MissingArgumentError(argName string) error { 17 | return MissingArgumentWithExplanationError(argName, "") 18 | } 19 | 20 | func MissingArgumentWithExplanationError(argName string, explanation string) error { 21 | return errors.New(fmt.Sprintf("Missing argument `%v` must be specified. %v", argName, explanation)) 22 | } 23 | 24 | func EnsureTargetInConfig(cfg config.Config) error { 25 | if cfg.ActiveTargetName == "" { 26 | return errors.New(MISSING_TARGET) 27 | } 28 | return nil 29 | } 30 | 31 | func EnsureContextInConfig(cfg config.Config) error { 32 | if err := EnsureTargetInConfig(cfg); err != nil { 33 | return err 34 | } 35 | if cfg.GetActiveTarget().ActiveContextName == "" { 36 | return errors.New(MISSING_CONTEXT) 37 | } 38 | return nil 39 | } 40 | 41 | func NotifyValidationErrors(err error, cmd *cobra.Command, log Logger) { 42 | if err != nil { 43 | log.Error(err.Error()) 44 | cmd.Usage() 45 | os.Exit(1) 46 | } 47 | } 48 | 49 | func NotifyErrorsWithRetry(err error, log Logger, c config.Config) { 50 | if err != nil { 51 | switch t := err.(type) { 52 | case uaa.RequestError: 53 | log.Error(t.Error()) 54 | NewJsonPrinter(log).PrintError(t.ErrorResponse) 55 | default: 56 | log.Error(err.Error()) 57 | } 58 | verboseRetryMsg(log, c) 59 | os.Exit(1) 60 | } 61 | } 62 | 63 | func verboseRetryMsg(log Logger, c config.Config) { 64 | if !c.Verbose { 65 | log.Info("Retry with --verbose for more information.") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cli/implicit_client_impersonator.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "strconv" 8 | 9 | "code.cloudfoundry.org/uaa-cli/utils" 10 | "golang.org/x/oauth2" 11 | "time" 12 | ) 13 | 14 | type ClientImpersonator interface { 15 | Start() 16 | Authorize() 17 | Done() chan oauth2.Token 18 | } 19 | 20 | type ImplicitClientImpersonator struct { 21 | ClientID string 22 | TokenFormat string 23 | Scope string 24 | UaaBaseURL string 25 | Port int 26 | Log Logger 27 | AuthCallbackServer CallbackServer 28 | BrowserLauncher func(string) error 29 | done chan oauth2.Token 30 | } 31 | 32 | const CallbackCSS = `` 39 | const implicitCallbackJS = `` 47 | const implicitCallbackHTML = ` 48 |

Implicit Grant: Success

49 |

The UAA redirected you to this page with an access token.

50 |

The token has been added to the CLI's active context. You may close this window.

51 | ` 52 | 53 | func NewImplicitClientImpersonator(clientId, 54 | uaaBaseURL string, 55 | tokenFormat string, 56 | scope string, 57 | port int, 58 | log Logger, 59 | launcher func(string) error) ImplicitClientImpersonator { 60 | 61 | impersonator := ImplicitClientImpersonator{ 62 | ClientID: clientId, 63 | UaaBaseURL: uaaBaseURL, 64 | TokenFormat: tokenFormat, 65 | Scope: scope, 66 | Port: port, 67 | BrowserLauncher: launcher, 68 | Log: log, 69 | done: make(chan oauth2.Token), 70 | } 71 | 72 | callbackServer := NewAuthCallbackServer(implicitCallbackHTML, CallbackCSS, implicitCallbackJS, log, port) 73 | callbackServer.SetHangupFunc(func(done chan url.Values, values url.Values) { 74 | token := values.Get("access_token") 75 | if token != "" { 76 | done <- values 77 | } 78 | }) 79 | impersonator.AuthCallbackServer = callbackServer 80 | 81 | return impersonator 82 | } 83 | 84 | func (ici ImplicitClientImpersonator) Start() { 85 | go func() { 86 | urlValues := make(chan url.Values) 87 | go ici.AuthCallbackServer.Start(urlValues) 88 | values := <-urlValues 89 | response := oauth2.Token{ 90 | AccessToken: values.Get("access_token"), 91 | TokenType: values.Get("token_type"), 92 | } 93 | response = *response.WithExtra(map[string]interface{}{ 94 | "scope": values.Get("scope"), 95 | "jti": values.Get("jti"), 96 | }) 97 | 98 | expiresIn, err := strconv.Atoi(values.Get("expires_in")) 99 | if err == nil { 100 | response.Expiry = time.Now().Add(time.Duration(expiresIn) * time.Second) 101 | } 102 | ici.Done() <- response 103 | }() 104 | } 105 | func (ici ImplicitClientImpersonator) Authorize() { 106 | requestValues := url.Values{} 107 | requestValues.Add("response_type", "token") 108 | requestValues.Add("client_id", ici.ClientID) 109 | requestValues.Add("scope", ici.Scope) 110 | requestValues.Add("token_format", ici.TokenFormat) 111 | requestValues.Add("redirect_uri", fmt.Sprintf("http://localhost:%v", ici.Port)) 112 | 113 | authUrl, err := utils.BuildUrl(ici.UaaBaseURL, "/oauth/authorize") 114 | if err != nil { 115 | ici.Log.Error("Something went wrong while building the authorization URL.") 116 | os.Exit(1) 117 | } 118 | authUrl.RawQuery = requestValues.Encode() 119 | 120 | ici.Log.Info("Launching browser window to " + authUrl.String()) 121 | ici.BrowserLauncher(authUrl.String()) 122 | } 123 | func (ici ImplicitClientImpersonator) Done() chan oauth2.Token { 124 | return ici.done 125 | } 126 | -------------------------------------------------------------------------------- /cli/implicit_client_impersonator_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | . "code.cloudfoundry.org/uaa-cli/cli" 5 | 6 | "io/ioutil" 7 | "net/url" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | type TestLauncher struct { 15 | TargetUrl string 16 | } 17 | 18 | func (tl *TestLauncher) Run(target string) error { 19 | tl.TargetUrl = target 20 | return nil 21 | } 22 | 23 | var _ = Describe("ImplicitClientImpersonator", func() { 24 | var ( 25 | impersonator ImplicitClientImpersonator 26 | logger Logger 27 | ) 28 | 29 | BeforeEach(func() { 30 | logger = NewLogger(ioutil.Discard, ioutil.Discard, ioutil.Discard, ioutil.Discard) 31 | }) 32 | 33 | Describe("NewImplicitClientImpersonator", func() { 34 | BeforeEach(func() { 35 | launcher := TestLauncher{} 36 | impersonator = NewImplicitClientImpersonator("implicitId", "http://uaa.com", "jwt", "openid", 9090, logger, launcher.Run) 37 | }) 38 | 39 | Describe("configures an AuthCallbackListener", func() { 40 | It("with appropriate static content", func() { 41 | Expect(impersonator.AuthCallbackServer.Javascript()).To(ContainSubstring("XMLHttpRequest")) 42 | Expect(impersonator.AuthCallbackServer.CSS()).To(ContainSubstring("Source Sans Pro")) 43 | Expect(impersonator.AuthCallbackServer.Html()).To(ContainSubstring("Implicit Grant: Success")) 44 | }) 45 | 46 | It("with the desired port", func() { 47 | Expect(impersonator.AuthCallbackServer.Port()).To(Equal(9090)) 48 | }) 49 | 50 | It("with its logger", func() { 51 | Expect(impersonator.AuthCallbackServer.Log()).NotTo(Equal(Logger{})) 52 | Expect(impersonator.AuthCallbackServer.Log()).To(Equal(logger)) 53 | }) 54 | 55 | It("with hangup func that looks for access_token in query params", func() { 56 | done := make(chan url.Values) 57 | 58 | urlParams := url.Values{} 59 | urlParams.Add("access_token", "56575db17b164e568668c0085ed14ae1") 60 | go impersonator.AuthCallbackServer.Hangup(done, urlParams) 61 | 62 | Expect(<-done).To(Equal(urlParams)) 63 | }) 64 | }) 65 | }) 66 | 67 | Describe("#Start", func() { 68 | BeforeEach(func() { 69 | launcher := TestLauncher{} 70 | impersonator = NewImplicitClientImpersonator("implicitId", "http://uaa.com", "jwt", "openid", 9090, logger, launcher.Run) 71 | impersonator.AuthCallbackServer = FakeCallbackServer{} 72 | }) 73 | 74 | It("starts the AuthCallbackServer", func() { 75 | go impersonator.Start() 76 | tokenResponse := <-impersonator.Done() 77 | Expect(tokenResponse.AccessToken).To(Equal("a_fake_token")) 78 | Expect(tokenResponse.TokenType).To(Equal("bearer")) 79 | Expect(tokenResponse.Extra("scope")).To(Equal("openid")) 80 | Expect(tokenResponse.Extra("jti")).To(Equal("jti_value")) 81 | Expect(tokenResponse.Expiry).Should(BeTemporally("~", time.Now(), 4000*time.Second)) 82 | }) 83 | }) 84 | 85 | Describe("#Authorize", func() { 86 | It("launches a browser to the authorize page", func() { 87 | launcher := TestLauncher{} 88 | impersonator = NewImplicitClientImpersonator("implicitId", "http://uaa.com", "jwt", "openid", 9090, logger, launcher.Run) 89 | 90 | impersonator.Authorize() 91 | 92 | Expect(launcher.TargetUrl).To(Equal("http://uaa.com/oauth/authorize?client_id=implicitId&redirect_uri=http%3A%2F%2Flocalhost%3A9090&response_type=token&scope=openid&token_format=jwt")) 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /cli/interactive_prompt.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | "syscall" 10 | 11 | "github.com/fatih/color" 12 | "golang.org/x/crypto/ssh/terminal" 13 | ) 14 | 15 | var InteractiveOutput io.Writer = os.Stdout 16 | var InteractiveInput io.Reader = os.Stdin 17 | var ReadPassword func(fd int) ([]byte, error) = terminal.ReadPassword 18 | 19 | type InteractiveSecret struct { 20 | Prompt string 21 | } 22 | 23 | func (is *InteractiveSecret) Get() (string, error) { 24 | fmt.Fprint(InteractiveOutput, is.Prompt+": ") 25 | 26 | bytePassword, err := ReadPassword(int(syscall.Stdin)) 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | return string(bytePassword), nil 32 | } 33 | 34 | type InteractivePrompt struct { 35 | Prompt string 36 | } 37 | 38 | func (ip InteractivePrompt) Get() (string, error) { 39 | prompt := color.CyanString(ip.Prompt + ": ") 40 | fmt.Fprint(InteractiveOutput, prompt) 41 | 42 | reader := bufio.NewReader(InteractiveInput) 43 | val, err := reader.ReadString('\n') 44 | if err != nil { 45 | return "", err 46 | } 47 | return strings.TrimSpace(val), nil 48 | } 49 | -------------------------------------------------------------------------------- /cli/interactive_prompt_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | . "code.cloudfoundry.org/uaa-cli/cli" 5 | 6 | "bufio" 7 | "bytes" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("Interactive inputs", func() { 14 | var inbuf *bytes.Buffer 15 | var outbuf *bytes.Buffer 16 | 17 | BeforeEach(func() { 18 | inbuf = bytes.NewBuffer([]byte{}) 19 | outbuf = bytes.NewBuffer([]byte{}) 20 | InteractiveOutput = outbuf 21 | InteractiveInput = inbuf 22 | }) 23 | 24 | Describe("InteractivePrompt", func() { 25 | It("prints the prompt for the user", func() { 26 | ip := InteractivePrompt{Prompt: "Username"} 27 | 28 | ip.Get() 29 | 30 | outreader := bufio.NewReader(outbuf) 31 | printed, err := outreader.ReadString(':') 32 | Expect(err).NotTo(HaveOccurred()) 33 | Expect(printed).To(ContainSubstring("Username:")) 34 | }) 35 | 36 | It("gets user input", func() { 37 | inbuf.WriteString("woodstock\n") 38 | 39 | ip := InteractivePrompt{Prompt: "Username"} 40 | input, err := ip.Get() 41 | 42 | Expect(err).NotTo(HaveOccurred()) 43 | Expect(input).To(Equal("woodstock")) 44 | }) 45 | }) 46 | 47 | Describe("InteractiveSecret", func() { 48 | It("gets user input with terminal.ReadPassword", func() { 49 | ReadPassword = func(fd int) ([]byte, error) { 50 | return []byte("somepassword"), nil 51 | } 52 | 53 | ip := InteractiveSecret{Prompt: "Password"} 54 | input, err := ip.Get() 55 | 56 | Expect(err).NotTo(HaveOccurred()) 57 | Expect(input).To(Equal("somepassword")) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /cli/logger.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fatih/color" 6 | "io" 7 | "log" 8 | ) 9 | 10 | type Logger struct { 11 | infoLog *log.Logger 12 | // robots is like info, but should only receive machine-parsable output 13 | robotLog *log.Logger 14 | warnLog *log.Logger 15 | errorLog *log.Logger 16 | muted bool 17 | } 18 | 19 | func NewLogger(infoHandle, robotsHandle, warningHandle, errorHandle io.Writer) Logger { 20 | infoLog := log.New(infoHandle, "", 0) 21 | robotLog := log.New(robotsHandle, "", 0) 22 | warnLog := log.New(warningHandle, "", 0) 23 | errorLog := log.New(errorHandle, "", 0) 24 | return Logger{infoLog, robotLog, warnLog, errorLog, false} 25 | } 26 | 27 | func (l *Logger) Info(msg string) { 28 | if l.muted { 29 | return 30 | } 31 | l.infoLog.Println(msg) 32 | } 33 | func (l *Logger) Infof(format string, a ...interface{}) { 34 | l.Info(fmt.Sprintf(format, a...)) 35 | } 36 | 37 | func (l *Logger) Warn(msg string) { 38 | if l.muted { 39 | return 40 | } 41 | yellow := color.New(color.FgYellow).SprintFunc() 42 | l.warnLog.Println(yellow(msg)) 43 | } 44 | 45 | func (l *Logger) Error(msg string) { 46 | if l.muted { 47 | return 48 | } 49 | red := color.New(color.FgRed).SprintFunc() 50 | l.errorLog.Println(red(msg)) 51 | } 52 | func (l *Logger) Errorf(format string, a ...interface{}) { 53 | l.Error(fmt.Sprintf(format, a...)) 54 | } 55 | 56 | func (l *Logger) Robots(msg string) { 57 | if l.muted { 58 | return 59 | } 60 | l.robotLog.Println(msg) 61 | } 62 | func (l *Logger) Robotsf(format string, a ...interface{}) { 63 | l.Robots(fmt.Sprintf(format, a...)) 64 | } 65 | func (l *Logger) Mute() { 66 | l.muted = true 67 | } 68 | func (l *Logger) Unmute() { 69 | l.muted = false 70 | } 71 | -------------------------------------------------------------------------------- /cli/printer.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | type Printer interface { 9 | Print(interface{}) error 10 | } 11 | 12 | type JsonPrinter struct { 13 | Log Logger 14 | } 15 | 16 | func NewJsonPrinter(log Logger) JsonPrinter { 17 | return JsonPrinter{log} 18 | } 19 | func (jp JsonPrinter) Print(obj interface{}) error { 20 | j, err := json.MarshalIndent(&obj, "", " ") 21 | if err != nil { 22 | return err 23 | } 24 | jp.Log.Robots(string(j)) 25 | return nil 26 | } 27 | 28 | func (jp JsonPrinter) PrintError(b []byte) error { 29 | var out bytes.Buffer 30 | err := json.Indent(&out, b, "", " ") 31 | if err != nil { 32 | return err 33 | } 34 | jp.Log.Error(out.String()) 35 | return nil 36 | } 37 | 38 | type TestPrinter struct { 39 | CallData map[string]interface{} 40 | } 41 | 42 | func NewTestPrinter() TestPrinter { 43 | tp := TestPrinter{} 44 | tp.CallData = make(map[string]interface{}) 45 | return tp 46 | } 47 | func (tp TestPrinter) Print(obj interface{}) error { 48 | tp.CallData["Print"] = obj 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /cli/printer_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | "bytes" 5 | . "code.cloudfoundry.org/uaa-cli/cli" 6 | "encoding/json" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | . "github.com/onsi/gomega/gbytes" 10 | ) 11 | 12 | var _ = Describe("JsonPrinter", func() { 13 | var infoLogBuf, robotLogBuf, warnLogBuf, errorLogBuf *Buffer 14 | var printer JsonPrinter 15 | 16 | BeforeEach(func() { 17 | infoLogBuf, robotLogBuf, warnLogBuf, errorLogBuf = NewBuffer(), NewBuffer(), NewBuffer(), NewBuffer() 18 | printer = NewJsonPrinter(NewLogger(infoLogBuf, robotLogBuf, warnLogBuf, errorLogBuf)) 19 | }) 20 | Describe("Print", func() { 21 | It("prints things to the Robots log", func() { 22 | printer.Print(struct { 23 | Foo string 24 | Bar string 25 | }{"foo", "bar"}) 26 | 27 | Expect(robotLogBuf.Contents()).To(MatchJSON(`{"Foo":"foo","Bar":"bar"}`)) 28 | }) 29 | 30 | It("returns error when cannot marhsal into json", func() { 31 | unJsonifiableObj := make(chan bool) 32 | err := printer.Print(unJsonifiableObj) 33 | 34 | Expect(err).To(HaveOccurred()) 35 | }) 36 | }) 37 | 38 | Describe("PrintError", func() { 39 | It("prints a json buffer to Error log", func() { 40 | jsonData := struct { 41 | Foo string 42 | Bar string 43 | }{"foo", "bar"} 44 | jsonRaw, _ := json.Marshal(jsonData) 45 | var out bytes.Buffer 46 | _ = json.Indent(&out, jsonRaw, "", " ") 47 | 48 | printer.PrintError(jsonRaw) 49 | 50 | Expect(string(errorLogBuf.Contents())).To(ContainSubstring(out.String())) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /cmd/activate_user.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "errors" 6 | 7 | "code.cloudfoundry.org/uaa-cli/config" 8 | "code.cloudfoundry.org/uaa-cli/utils" 9 | "github.com/cloudfoundry-community/go-uaa" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func ActivateUserCmd(api *uaa.API, username, origin, attributes string) error { 14 | user, err := api.GetUserByUsername(username, origin, attributes) 15 | if err != nil { 16 | return err 17 | } 18 | if user.Meta == nil { 19 | return errors.New("The user did not have expected metadata version.") 20 | } 21 | err = api.ActivateUser(user.ID, user.Meta.Version) 22 | if err != nil { 23 | return err 24 | } 25 | log.Infof("Account for user %v successfully activated.", utils.Emphasize(user.Username)) 26 | 27 | return nil 28 | } 29 | 30 | func ActivateUserValidations(cfg config.Config, args []string) error { 31 | if err := cli.EnsureContextInConfig(cfg); err != nil { 32 | return err 33 | } 34 | 35 | if len(args) == 0 { 36 | return errors.New("The positional argument USERNAME must be specified.") 37 | } 38 | return nil 39 | } 40 | 41 | var activateUserCmd = &cobra.Command{ 42 | Use: "activate-user USERNAME", 43 | Short: "Activate a user by username", 44 | PreRun: func(cmd *cobra.Command, args []string) { 45 | err := ActivateUserValidations(GetSavedConfig(), args) 46 | cli.NotifyValidationErrors(err, cmd, log) 47 | }, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | err := ActivateUserCmd(GetAPIFromSavedTokenInContext(), args[0], origin, attributes) 50 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 51 | }, 52 | } 53 | 54 | func init() { 55 | RootCmd.AddCommand(activateUserCmd) 56 | activateUserCmd.Annotations = make(map[string]string) 57 | activateUserCmd.Annotations[USER_CRUD_CATEGORY] = "true" 58 | } 59 | -------------------------------------------------------------------------------- /cmd/activate_user_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "code.cloudfoundry.org/uaa-cli/config" 7 | "code.cloudfoundry.org/uaa-cli/fixtures" 8 | "github.com/cloudfoundry-community/go-uaa" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | . "github.com/onsi/gomega/gbytes" 12 | . "github.com/onsi/gomega/gexec" 13 | . "github.com/onsi/gomega/ghttp" 14 | ) 15 | 16 | var _ = Describe("ActivateUser", func() { 17 | BeforeEach(func() { 18 | c := config.NewConfigWithServerURL(server.URL()) 19 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 20 | c.AddContext(ctx) 21 | Expect(config.WriteConfig(c)).Should(Succeed()) 22 | }) 23 | 24 | It("activates a user", func() { 25 | server.RouteToHandler("GET", "/Users", CombineHandlers( 26 | VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22woodstock%40peanuts.com%22&startIndex=1"), 27 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})), 28 | )) 29 | server.RouteToHandler("PATCH", "/Users/abcdef", CombineHandlers( 30 | VerifyRequest("PATCH", "/Users/abcdef", ""), 31 | VerifyHeaderKV("If-Match", "10"), 32 | VerifyJSON(`{"active": true}`), 33 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com"})), 34 | )) 35 | 36 | session := runCommand("activate-user", "woodstock@peanuts.com") 37 | 38 | Expect(server.ReceivedRequests()).To(HaveLen(2)) 39 | Eventually(session).Should(Exit(0)) 40 | Expect(session.Out).To(Say("Account for user woodstock@peanuts.com successfully activated.")) 41 | }) 42 | 43 | Describe("validations", func() { 44 | It("requires a target", func() { 45 | config.WriteConfig(config.NewConfig()) 46 | 47 | session := runCommand("activate-user", "woodstock@peanuts.com") 48 | 49 | Expect(session.Err).To(Say("You must set a target in order to use this command.")) 50 | Expect(session).Should(Exit(1)) 51 | }) 52 | 53 | It("requires a context", func() { 54 | cfg := config.NewConfigWithServerURL(server.URL()) 55 | config.WriteConfig(cfg) 56 | 57 | session := runCommand("activate-user", "woodstock@peanuts.com") 58 | 59 | Expect(session.Err).To(Say("You must have a token in your context to perform this command.")) 60 | Expect(session).Should(Exit(1)) 61 | }) 62 | 63 | It("requires a username", func() { 64 | c := config.NewConfigWithServerURL(server.URL()) 65 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 66 | c.AddContext(ctx) 67 | config.WriteConfig(c) 68 | 69 | session := runCommand("activate-user") 70 | 71 | Expect(session.Err).To(Say("The positional argument USERNAME must be specified.")) 72 | Expect(session).Should(Exit(1)) 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /cmd/add_member.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | cli_config "code.cloudfoundry.org/uaa-cli/config" 6 | "code.cloudfoundry.org/uaa-cli/utils" 7 | "errors" 8 | "github.com/cloudfoundry-community/go-uaa" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func AddMemberPreRunValidations(config cli_config.Config, args []string) error { 13 | if err := cli.EnsureContextInConfig(config); err != nil { 14 | return err 15 | } 16 | 17 | if len(args) != 2 { 18 | return errors.New("The positional arguments GROUPNAME and USERNAME must be specified.") 19 | } 20 | 21 | return nil 22 | } 23 | 24 | func AddMemberCmd(api *uaa.API, groupName, username string, log cli.Logger) error { 25 | group, err := api.GetGroupByName(groupName, "") 26 | if err != nil { 27 | return err 28 | } 29 | 30 | user, err := api.GetUserByUsername(username, "", "") 31 | if err != nil { 32 | return err 33 | } 34 | 35 | err = api.AddGroupMember(group.ID, user.ID, "", "") 36 | if err != nil { 37 | return err 38 | } 39 | 40 | log.Infof("User %v successfully added to group %v", utils.Emphasize(username), utils.Emphasize(groupName)) 41 | 42 | return nil 43 | } 44 | 45 | var addMemberCmd = &cobra.Command{ 46 | Use: "add-member GROUPNAME USERNAME", 47 | Short: "Add a user to a group", 48 | PreRun: func(cmd *cobra.Command, args []string) { 49 | cfg := GetSavedConfig() 50 | cli.NotifyValidationErrors(AddMemberPreRunValidations(cfg, args), cmd, log) 51 | }, 52 | Run: func(cmd *cobra.Command, args []string) { 53 | groupName := args[0] 54 | userName := args[1] 55 | cli.NotifyErrorsWithRetry(AddMemberCmd(GetAPIFromSavedTokenInContext(), groupName, userName, log), log, GetSavedConfig()) 56 | }, 57 | } 58 | 59 | func init() { 60 | RootCmd.AddCommand(addMemberCmd) 61 | addMemberCmd.Annotations = make(map[string]string) 62 | addMemberCmd.Annotations[GROUP_CRUD_CATEGORY] = "true" 63 | } 64 | -------------------------------------------------------------------------------- /cmd/add_member_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "code.cloudfoundry.org/uaa-cli/fixtures" 7 | "github.com/cloudfoundry-community/go-uaa" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | . "github.com/onsi/gomega/gbytes" 11 | . "github.com/onsi/gomega/gexec" 12 | . "github.com/onsi/gomega/ghttp" 13 | "net/http" 14 | ) 15 | 16 | var _ = Describe("AddMember", func() { 17 | Describe("and a target was previously set", func() { 18 | BeforeEach(func() { 19 | c := config.NewConfigWithServerURL(server.URL()) 20 | c.AddContext(config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 21 | config.WriteConfig(c) 22 | }) 23 | 24 | It("creates a membership in a group", func() { 25 | membershipJson := `{"origin":"uaa","type":"USER","value":"fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70"}` 26 | 27 | server.RouteToHandler("POST", "/Groups/05a0c169-3592-4a45-b109-a16d9246e0ab/members", CombineHandlers( 28 | VerifyRequest("POST", "/Groups/05a0c169-3592-4a45-b109-a16d9246e0ab/members"), 29 | VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"), 30 | VerifyHeaderKV("Accept", "application/json"), 31 | VerifyJSON(membershipJson), 32 | RespondWith(http.StatusOK, membershipJson, contentTypeJson), 33 | )) 34 | server.RouteToHandler("GET", "/Groups", CombineHandlers( 35 | VerifyRequest("GET", "/Groups", "filter=displayName+eq+%22uaa.admin%22&count=100&startIndex=1"), 36 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.Group{ID: "05a0c169-3592-4a45-b109-a16d9246e0ab", DisplayName: "uaa.admin"})), 37 | )) 38 | server.RouteToHandler("GET", "/Users", CombineHandlers( 39 | VerifyRequest("GET", "/Users", "filter=userName+eq+%22woodstock@peanuts.com%22&count=100&startIndex=1"), 40 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{ID: "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", Username: "woodstock@peanuts.com"})), 41 | )) 42 | 43 | session := runCommand("add-member", "uaa.admin", "woodstock@peanuts.com") 44 | 45 | Eventually(session).Should(Exit(0)) 46 | Expect(session).To(Say("User woodstock@peanuts.com successfully added to group uaa.admin")) 47 | }) 48 | }) 49 | 50 | Describe("when no target was previously set", func() { 51 | BeforeEach(func() { 52 | c := config.Config{} 53 | config.WriteConfig(c) 54 | }) 55 | 56 | It("tells the user to set a target", func() { 57 | session := runCommand("add-member", "uaa.admin", "woodstock") 58 | 59 | Eventually(session).Should(Exit(1)) 60 | Expect(session.Err).To(Say(cli.MISSING_TARGET)) 61 | }) 62 | }) 63 | 64 | Describe("when no token in context", func() { 65 | BeforeEach(func() { 66 | c := config.NewConfigWithServerURL(server.URL()) 67 | config.WriteConfig(c) 68 | }) 69 | 70 | It("tells the user to get a token", func() { 71 | session := runCommand("add-member", "uaa.admin", "woodstock") 72 | 73 | Eventually(session).Should(Exit(1)) 74 | Expect(session.Err).To(Say(cli.MISSING_CONTEXT)) 75 | }) 76 | }) 77 | 78 | Describe("validations", func() { 79 | It("only accepts groupname and username", func() { 80 | session := runCommand("add-member", "first-arg", "second-arg", "third-arg") 81 | Eventually(session).Should(Exit(1)) 82 | 83 | session = runCommand("add-member", "woodstock") 84 | Eventually(session).Should(Exit(1)) 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /cmd/api_client.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/http" 5 | 6 | "code.cloudfoundry.org/uaa-cli/config" 7 | "github.com/cloudfoundry-community/go-uaa" 8 | ) 9 | 10 | func GetAPIFromSavedTokenInContext() *uaa.API { 11 | cfg := GetSavedConfig() 12 | token := cfg.GetActiveContext().Token 13 | api, err := uaa.New( 14 | cfg.GetActiveTarget().BaseUrl, 15 | uaa.WithToken(&token), 16 | uaa.WithZoneID(cfg.ZoneSubdomain), 17 | uaa.WithSkipSSLValidation(cfg.GetActiveTarget().SkipSSLValidation), 18 | uaa.WithVerbosity(verbose), 19 | ) 20 | if err != nil { 21 | panic(err) 22 | } 23 | return api 24 | } 25 | 26 | func GetUnauthenticatedAPI() *uaa.API { 27 | cfg := GetSavedConfig() 28 | return GetUnauthenticatedAPIFromConfig(cfg) 29 | } 30 | 31 | func GetUnauthenticatedAPIFromConfig(cfg config.Config) *uaa.API { 32 | client := &http.Client{Transport: http.DefaultTransport} 33 | api, err := uaa.New( 34 | cfg.GetActiveTarget().BaseUrl, 35 | uaa.WithNoAuthentication(), 36 | uaa.WithZoneID(cfg.ZoneSubdomain), 37 | uaa.WithSkipSSLValidation(cfg.GetActiveTarget().SkipSSLValidation), 38 | uaa.WithVerbosity(verbose), 39 | uaa.WithClient(client), 40 | ) 41 | if err != nil { 42 | panic(err) 43 | } 44 | return api 45 | } 46 | -------------------------------------------------------------------------------- /cmd/cmd_suite_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | . "github.com/onsi/gomega/gbytes" 7 | . "github.com/onsi/gomega/gexec" 8 | . "github.com/onsi/gomega/ghttp" 9 | 10 | "code.cloudfoundry.org/uaa-cli/config" 11 | "io" 12 | "io/ioutil" 13 | "net/http" 14 | "os" 15 | "os/exec" 16 | "runtime" 17 | "testing" 18 | ) 19 | 20 | func TestCmd(t *testing.T) { 21 | RegisterFailHandler(Fail) 22 | RunSpecs(t, "Cmd Suite") 23 | } 24 | 25 | var ( 26 | commandPath string 27 | homeDir string 28 | server *Server 29 | AuthCallbackTimeout float64 = 5 30 | AuthCallbackPollInterval float64 = 0.01 31 | ) 32 | 33 | var contentTypeJson = http.Header{ 34 | "Content-Type": []string{"application/json"}, 35 | } 36 | 37 | var _ = BeforeEach(func() { 38 | var err error 39 | homeDir, err = ioutil.TempDir("", "uaa-test") 40 | Expect(err).NotTo(HaveOccurred()) 41 | server = NewServer() 42 | 43 | if runtime.GOOS == "windows" { 44 | os.Setenv("USERPROFILE", homeDir) 45 | } else { 46 | os.Setenv("HOME", homeDir) 47 | } 48 | }) 49 | 50 | var _ = AfterEach(func() { 51 | os.RemoveAll(homeDir) 52 | config.RemoveConfig() 53 | server.Close() 54 | }) 55 | 56 | var _ = SynchronizedBeforeSuite(func() []byte { 57 | executable_path, err := Build("code.cloudfoundry.org/uaa-cli", "-ldflags", "-X code.cloudfoundry.org/uaa-cli/version.Version=test-version") 58 | Expect(err).NotTo(HaveOccurred()) 59 | return []byte(executable_path) 60 | }, func(data []byte) { 61 | commandPath = string(data) 62 | }) 63 | 64 | var _ = SynchronizedAfterSuite(func() {}, func() { 65 | CleanupBuildArtifacts() 66 | }) 67 | 68 | func runCommand(args ...string) *Session { 69 | cmd := exec.Command(commandPath, args...) 70 | session, err := Start(cmd, GinkgoWriter, GinkgoWriter) 71 | Expect(err).NotTo(HaveOccurred()) 72 | <-session.Exited 73 | 74 | return session 75 | } 76 | 77 | func runCommandWithEnv(env []string, args ...string) *Session { 78 | cmd := exec.Command(commandPath, args...) 79 | existing := os.Environ() 80 | for _, env_var := range env { 81 | existing = append(existing, env_var) 82 | } 83 | cmd.Env = existing 84 | session, err := Start(cmd, GinkgoWriter, GinkgoWriter) 85 | Expect(err).NotTo(HaveOccurred()) 86 | <-session.Exited 87 | 88 | return session 89 | } 90 | 91 | func runCommandWithStdin(stdin io.Reader, args ...string) *Session { 92 | cmd := exec.Command(commandPath, args...) 93 | cmd.Stdin = stdin 94 | session, err := Start(cmd, GinkgoWriter, GinkgoWriter) 95 | Expect(err).NotTo(HaveOccurred()) 96 | <-session.Exited 97 | 98 | return session 99 | } 100 | 101 | func ItBehavesLikeHelp(command string, alias string, validate func(*Session)) { 102 | It("displays help", func() { 103 | session := runCommand(command, "-h") 104 | Eventually(session).Should(Exit(1)) 105 | validate(session) 106 | }) 107 | 108 | It("displays help using the alias", func() { 109 | session := runCommand(alias, "-h") 110 | Eventually(session).Should(Exit(1)) 111 | validate(session) 112 | }) 113 | } 114 | 115 | func ItSupportsTheVerboseFlagWhenGet(command string, endpoint string, responseJson string) { 116 | It("shows extra output about the request on success", func() { 117 | server.RouteToHandler("GET", endpoint, 118 | RespondWith(http.StatusOK, responseJson, contentTypeJson), 119 | ) 120 | 121 | session := runCommand(command, "--verbose") 122 | 123 | Eventually(session).Should(Exit(0)) 124 | Expect(session.Out).To(Say("GET " + endpoint)) 125 | Expect(session.Out).To(Say("Accept: application/json")) 126 | Expect(session.Out).To(Say("200 OK")) 127 | }) 128 | 129 | It("shows extra output about the request on error", func() { 130 | server.RouteToHandler("GET", endpoint, 131 | RespondWith(http.StatusBadRequest, "garbage response"), 132 | ) 133 | 134 | session := runCommand(command, "--verbose") 135 | 136 | Eventually(session).Should(Exit(1)) 137 | Expect(session.Out).To(Say("GET " + endpoint)) 138 | Expect(session.Out).To(Say("Accept: application/json")) 139 | Expect(session.Out).To(Say("400 Bad Request")) 140 | Expect(session.Out).To(Say("garbage response")) 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /cmd/context.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/help" 6 | "github.com/spf13/cobra" 7 | "os" 8 | ) 9 | 10 | var contextCmd = &cobra.Command{ 11 | Use: "context", 12 | Short: "See information about the currently active CLI context", 13 | Long: help.Context(), 14 | Run: func(cmd *cobra.Command, args []string) { 15 | c := GetSavedConfig() 16 | 17 | if c.ActiveTargetName == "" { 18 | log.Error("No context is currently set.") 19 | log.Error(`To get started, target a UAA and fetch a token. See "uaa target -h" for details.`) 20 | os.Exit(1) 21 | } 22 | 23 | if len(c.GetActiveTarget().Contexts) == 0 { 24 | log.Error("No context is currently set.") 25 | log.Error(`Use a token command such as "uaa get-password-token" or "uaa get-client-credentials-token" to fetch a token.`) 26 | os.Exit(1) 27 | } 28 | 29 | activeContext := c.GetActiveContext() 30 | err := cli.NewJsonPrinter(log).Print(activeContext) 31 | if err != nil { 32 | log.Error(err.Error()) 33 | os.Exit(1) 34 | } 35 | }, 36 | } 37 | 38 | func init() { 39 | RootCmd.AddCommand(contextCmd) 40 | contextCmd.Annotations = make(map[string]string) 41 | contextCmd.Annotations[INTRO_CATEGORY] = "true" 42 | } 43 | -------------------------------------------------------------------------------- /cmd/context_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/config" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | . "github.com/onsi/gomega/gbytes" 8 | . "github.com/onsi/gomega/gexec" 9 | ) 10 | 11 | var _ = Describe("Context", func() { 12 | Describe("when no target was previously set", func() { 13 | BeforeEach(func() { 14 | c := config.NewConfig() 15 | config.WriteConfig(c) 16 | }) 17 | 18 | It("tells the user to set a target", func() { 19 | session := runCommand("context") 20 | 21 | Expect(session.Err).To(Say("No context is currently set.")) 22 | Expect(session.Err).To(Say(`To get started, target a UAA and fetch a token. See "uaa target -h" for details`)) 23 | Eventually(session).Should(Exit(1)) 24 | }) 25 | }) 26 | 27 | Describe("when a target was previously set but there is no active context", func() { 28 | BeforeEach(func() { 29 | c := config.NewConfigWithServerURL("http://login.somewhere.com") 30 | config.WriteConfig(c) 31 | }) 32 | 33 | It("tells the user to set a context", func() { 34 | session := runCommand("context") 35 | 36 | Expect(session.Err).To(Say("No context is currently set.")) 37 | Expect(session.Err).To(Say(`Use a token command such as "uaa get-password-token" or "uaa get-client-credentials-token" to fetch a token.`)) 38 | Eventually(session).Should(Exit(1)) 39 | }) 40 | }) 41 | 42 | Describe("when there is an active context", func() { 43 | BeforeEach(func() { 44 | c := config.NewConfigWithServerURL("http://login.somewhere.com") 45 | ctx := config.UaaContext{ClientId: "admin", Username: "woodstock"} 46 | c.AddContext(ctx) 47 | config.WriteConfig(c) 48 | }) 49 | 50 | It("displays the context", func() { 51 | activeContextJson := `{ 52 | "client_id": "admin", 53 | "grant_type": "", 54 | "username": "woodstock", 55 | "Token": { 56 | "access_token": "", 57 | "expiry": "0001-01-01T00:00:00Z" 58 | } 59 | }` 60 | session := runCommand("context") 61 | 62 | Expect(session.Out.Contents()).To(MatchJSON(activeContextJson)) 63 | Eventually(session).Should(Exit(0)) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /cmd/contexts.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/help" 5 | "github.com/olekukonko/tablewriter" 6 | "github.com/spf13/cobra" 7 | "os" 8 | ) 9 | 10 | var contextsCmd = cobra.Command{ 11 | Use: "contexts", 12 | Short: "List available contexts for the currently targeted UAA", 13 | Long: help.Context(), 14 | Run: func(cmd *cobra.Command, args []string) { 15 | c := GetSavedConfig() 16 | 17 | if c.ActiveTargetName == "" { 18 | log.Error("No contexts are currently available.") 19 | log.Error(`To get started, target a UAA and fetch a token. See "uaa target -h" for details.`) 20 | os.Exit(1) 21 | } 22 | 23 | if len(c.GetActiveTarget().Contexts) == 0 { 24 | log.Error("No contexts are currently available.") 25 | log.Error(`Use a token command such as "uaa get-password-token" or "uaa get-client-credentials-token" to fetch a token and create a context.`) 26 | os.Exit(1) 27 | } 28 | 29 | table := tablewriter.NewWriter(os.Stdout) 30 | table.Header("ClientID", "Username", "Grant Type") 31 | for _, context := range c.GetActiveTarget().Contexts { 32 | _ = table.Append(context.ClientId, context.Username, string(context.GrantType)) 33 | } 34 | _ = table.Render() 35 | }, 36 | } 37 | 38 | func init() { 39 | RootCmd.AddCommand(&contextsCmd) 40 | contextsCmd.Annotations = make(map[string]string) 41 | contextsCmd.Annotations[INTRO_CATEGORY] = "true" 42 | contextsCmd.Hidden = true 43 | } 44 | -------------------------------------------------------------------------------- /cmd/contexts_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/config" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | . "github.com/onsi/gomega/gbytes" 8 | . "github.com/onsi/gomega/gexec" 9 | ) 10 | 11 | var _ = Describe("Contexts", func() { 12 | Describe("when no target was previously set", func() { 13 | BeforeEach(func() { 14 | c := config.NewConfig() 15 | Expect(config.WriteConfig(c)).Error().ShouldNot(HaveOccurred()) 16 | }) 17 | 18 | It("tells the user to set a target", func() { 19 | session := runCommand("contexts") 20 | 21 | Expect(session.Err).To(Say("No contexts are currently available.")) 22 | Expect(session.Err).To(Say(`To get started, target a UAA and fetch a token. See "uaa target -h" for details.`)) 23 | Eventually(session).Should(Exit(1)) 24 | }) 25 | }) 26 | 27 | Describe("when a target was previously set but there is no active context", func() { 28 | BeforeEach(func() { 29 | c := config.NewConfigWithServerURL("http://login.somewhere.com") 30 | Expect(config.WriteConfig(c)).Error().ShouldNot(HaveOccurred()) 31 | }) 32 | 33 | It("tells the user to set a context", func() { 34 | session := runCommand("contexts") 35 | 36 | Expect(session.Err).To(Say("No contexts are currently available.")) 37 | Expect(session.Err).To(Say(`Use a token command such as "uaa get-password-token" or "uaa get-client-credentials-token" to fetch a token and create a context.`)) 38 | Eventually(session).Should(Exit(1)) 39 | }) 40 | }) 41 | 42 | Describe("when there are contexts", func() { 43 | BeforeEach(func() { 44 | c := config.NewConfigWithServerURL("http://login.somewhere.com") 45 | ctx1 := config.UaaContext{ClientId: "admin", Username: "woodstock", GrantType: config.PASSWORD} 46 | c.AddContext(ctx1) 47 | Expect(config.WriteConfig(c)).Error().ShouldNot(HaveOccurred()) 48 | }) 49 | 50 | It("prints a table of results", func() { 51 | session := runCommand("contexts") 52 | 53 | // Headings 54 | Expect(session.Out).Should(Say("CLIENT ID")) 55 | Expect(session.Out).Should(Say("USERNAME")) 56 | Expect(session.Out).Should(Say("GRANT TYPE")) 57 | 58 | Expect(session.Out).Should(Say("admin")) 59 | Expect(session.Out).Should(Say("woodstock")) 60 | Expect(session.Out).Should(Say("password")) 61 | 62 | Eventually(session).Should(Exit(0)) 63 | }) 64 | }) 65 | 66 | }) 67 | -------------------------------------------------------------------------------- /cmd/create_group.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/uaa-cli/cli" 7 | "code.cloudfoundry.org/uaa-cli/config" 8 | "github.com/cloudfoundry-community/go-uaa" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func CreateGroupCmd(api *uaa.API, printer cli.Printer, name, description string) error { 13 | toCreate := uaa.Group{ 14 | DisplayName: name, 15 | Description: description, 16 | } 17 | 18 | group, err := api.CreateGroup(toCreate) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return printer.Print(group) 24 | } 25 | 26 | func CreateGroupValidation(cfg config.Config, args []string) error { 27 | if err := cli.EnsureContextInConfig(cfg); err != nil { 28 | return err 29 | } 30 | if len(args) == 0 { 31 | return errors.New("The positional argument GROUPNAME must be specified.") 32 | } 33 | return nil 34 | } 35 | 36 | var createGroupCmd = &cobra.Command{ 37 | Use: "create-group GROUPNAME", 38 | Short: "Create a group", 39 | Aliases: []string{"add-group"}, 40 | PreRun: func(cmd *cobra.Command, args []string) { 41 | cli.NotifyValidationErrors(CreateGroupValidation(GetSavedConfig(), args), cmd, log) 42 | }, 43 | Run: func(cmd *cobra.Command, args []string) { 44 | api := NewApiFromSavedConfig() 45 | 46 | err := CreateGroupCmd(api, cli.NewJsonPrinter(log), args[0], groupDescription) 47 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 48 | }, 49 | } 50 | 51 | func init() { 52 | RootCmd.AddCommand(createGroupCmd) 53 | createGroupCmd.Annotations = make(map[string]string) 54 | createGroupCmd.Annotations[GROUP_CRUD_CATEGORY] = "true" 55 | 56 | createGroupCmd.Flags().StringVarP(&groupDescription, "description", "d", "", `a human-readable description`) 57 | createGroupCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in which to create the group") 58 | } 59 | -------------------------------------------------------------------------------- /cmd/create_user.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/uaa-cli/cli" 7 | "code.cloudfoundry.org/uaa-cli/config" 8 | "code.cloudfoundry.org/uaa-cli/utils" 9 | "github.com/cloudfoundry-community/go-uaa" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func buildEmails(emails []string) []uaa.Email { 14 | userEmails := []uaa.Email{} 15 | var newEmail uaa.Email 16 | for i, email := range emails { 17 | if i == 0 { 18 | newEmail = uaa.Email{Primary: utils.NewTrueP(), Value: email} 19 | } else { 20 | newEmail = uaa.Email{Primary: utils.NewFalseP(), Value: email} 21 | } 22 | userEmails = append(userEmails, newEmail) 23 | } 24 | return userEmails 25 | } 26 | 27 | func buildPhones(phones []string) []uaa.PhoneNumber { 28 | userPhoneNumbers := []uaa.PhoneNumber{} 29 | var phone uaa.PhoneNumber 30 | for _, number := range phones { 31 | phone = uaa.PhoneNumber{Value: number} 32 | userPhoneNumbers = append(userPhoneNumbers, phone) 33 | } 34 | return userPhoneNumbers 35 | } 36 | 37 | func CreateUserCmd(api *uaa.API, printer cli.Printer, username, familyName, givenName, password, origin string, emails []string, phones []string) error { 38 | toCreate := uaa.User{ 39 | Username: username, 40 | Password: password, 41 | Origin: origin, 42 | Name: &uaa.UserName{ 43 | FamilyName: familyName, 44 | GivenName: givenName, 45 | }, 46 | } 47 | 48 | toCreate.Emails = buildEmails(emails) 49 | toCreate.PhoneNumbers = buildPhones(phones) 50 | 51 | user, err := api.CreateUser(toCreate) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | return printer.Print(user) 57 | } 58 | 59 | func CreateUserValidation(cfg config.Config, args []string, familyName, givenName string, emails []string) error { 60 | if err := cli.EnsureContextInConfig(cfg); err != nil { 61 | return err 62 | } 63 | if len(args) == 0 { 64 | return errors.New("The positional argument USERNAME must be specified.") 65 | } 66 | if len(emails) == 0 { 67 | return cli.MissingArgumentError("email") 68 | } 69 | return nil 70 | } 71 | 72 | var createUserCmd = &cobra.Command{ 73 | Use: "create-user USERNAME", 74 | Short: "Create a user", 75 | Aliases: []string{"add-user"}, 76 | PreRun: func(cmd *cobra.Command, args []string) { 77 | cli.NotifyValidationErrors(CreateUserValidation(GetSavedConfig(), args, familyName, givenName, emails), cmd, log) 78 | }, 79 | Run: func(cmd *cobra.Command, args []string) { 80 | api := NewApiFromSavedConfig() 81 | 82 | err := CreateUserCmd(api, cli.NewJsonPrinter(log), args[0], familyName, givenName, userPassword, origin, emails, phoneNumbers) 83 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 84 | }, 85 | } 86 | 87 | func init() { 88 | RootCmd.AddCommand(createUserCmd) 89 | createUserCmd.Annotations = make(map[string]string) 90 | createUserCmd.Annotations[USER_CRUD_CATEGORY] = "true" 91 | 92 | createUserCmd.Flags().StringVarP(&familyName, "familyName", "", "", "family name (required)") 93 | createUserCmd.Flags().StringVarP(&givenName, "givenName", "", "", "given name (required)") 94 | createUserCmd.Flags().StringVarP(&userPassword, "password", "p", "", `user password (required for "uaa" origin)`) 95 | createUserCmd.Flags().StringVarP(&origin, "origin", "o", "uaa", "user origin") 96 | createUserCmd.Flags().StringSliceVarP(&emails, "email", "", []string{}, "email address (required, multiple may be specified)") 97 | createUserCmd.Flags().StringSliceVarP(&phoneNumbers, "phone", "", []string{}, "phone number (optional, multiple may be specified)") 98 | createUserCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in which to create the user") 99 | } 100 | -------------------------------------------------------------------------------- /cmd/curl.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "strings" 9 | 10 | "code.cloudfoundry.org/uaa-cli/cli" 11 | "code.cloudfoundry.org/uaa-cli/config" 12 | "github.com/cloudfoundry-community/go-uaa" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func GetCurlValidations(cfg config.Config, args []string) error { 17 | if err := cli.EnsureTargetInConfig(cfg); err != nil { 18 | return err 19 | } 20 | if len(args) < 1 { 21 | return cli.MissingArgumentError("path") 22 | } 23 | return nil 24 | } 25 | 26 | func CurlCmd(api *uaa.API, logger cli.Logger, path, method, data string, headers []string) error { 27 | resHeaders, resBody, status, err := api.Curl(path, method, data, headers) 28 | if err != nil { 29 | return err 30 | } 31 | bodyPrinter := logger.Info 32 | if strings.Contains(resHeaders, "application/json") { 33 | buffer := bytes.Buffer{} 34 | if err := json.Indent(&buffer, []byte(resBody), "", " "); err == nil { 35 | resBody = buffer.String() 36 | bodyPrinter = logger.Robots 37 | } 38 | } 39 | 40 | if status >= http.StatusBadRequest { 41 | return errors.New(resBody) 42 | } 43 | bodyPrinter(resBody) 44 | 45 | return nil 46 | } 47 | 48 | var curlCmd = &cobra.Command{ 49 | Use: "curl", 50 | Short: "CURL to a UAA endpoint", 51 | Run: func(cmd *cobra.Command, args []string) { 52 | cfg := GetSavedConfig() 53 | cli.NotifyValidationErrors(GetCurlValidations(cfg, args), cmd, log) 54 | 55 | api := NewApiFromSavedConfig() 56 | 57 | err := CurlCmd(api, log, args[0], method, data, headers) 58 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 59 | }, 60 | } 61 | 62 | func init() { 63 | RootCmd.AddCommand(curlCmd) 64 | curlCmd.Annotations = make(map[string]string) 65 | curlCmd.Annotations[MISC_CATEGORY] = "true" 66 | 67 | curlCmd.Flags().StringVarP(&method, "method", "X", "GET", "HTTP method (GET, POST, PUT, DELETE, etc)") 68 | curlCmd.Flags().StringVarP(&data, "data", "d", "", "HTTP data to include in the request body") 69 | curlCmd.Flags().StringSliceVarP(&headers, "header", "H", []string{}, "Custom headers to include in the request, flag can be specified multiple times") 70 | } 71 | -------------------------------------------------------------------------------- /cmd/deactivate_user.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "code.cloudfoundry.org/uaa-cli/utils" 7 | "errors" 8 | "github.com/cloudfoundry-community/go-uaa" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func DeactivateUserCmd(api *uaa.API, username, origin, attributes string) error { 13 | user, err := api.GetUserByUsername(username, origin, attributes) 14 | if err != nil { 15 | return err 16 | } 17 | if user.Meta == nil { 18 | return errors.New("The user did not have expected metadata version.") 19 | } 20 | err = api.DeactivateUser(user.ID, user.Meta.Version) 21 | if err != nil { 22 | return err 23 | } 24 | log.Infof("Account for user %v successfully deactivated.", utils.Emphasize(user.Username)) 25 | 26 | return nil 27 | } 28 | 29 | func DeactivateUserValidations(cfg config.Config, args []string) error { 30 | if err := cli.EnsureContextInConfig(cfg); err != nil { 31 | return err 32 | } 33 | 34 | if len(args) == 0 { 35 | return errors.New("The positional argument USERNAME must be specified.") 36 | } 37 | return nil 38 | } 39 | 40 | var deactivateUserCmd = &cobra.Command{ 41 | Use: "deactivate-user USERNAME", 42 | Short: "Deactivate a user by username", 43 | PreRun: func(cmd *cobra.Command, args []string) { 44 | cli.NotifyValidationErrors(DeactivateUserValidations(GetSavedConfig(), args), cmd, log) 45 | }, 46 | Run: func(cmd *cobra.Command, args []string) { 47 | cfg := GetSavedConfig() 48 | 49 | if zoneSubdomain == "" { 50 | zoneSubdomain = cfg.ZoneSubdomain 51 | } 52 | api := NewApiFromSavedConfig() 53 | err := DeactivateUserCmd(api, args[0], origin, attributes) 54 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 55 | }, 56 | } 57 | 58 | func init() { 59 | RootCmd.AddCommand(deactivateUserCmd) 60 | deactivateUserCmd.Annotations = make(map[string]string) 61 | deactivateUserCmd.Annotations[USER_CRUD_CATEGORY] = "true" 62 | 63 | deactivateUserCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain from which to deactivate the user") 64 | 65 | } 66 | -------------------------------------------------------------------------------- /cmd/deactivate_user_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/config" 5 | "code.cloudfoundry.org/uaa-cli/fixtures" 6 | "github.com/cloudfoundry-community/go-uaa" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | . "github.com/onsi/gomega/gbytes" 10 | . "github.com/onsi/gomega/gexec" 11 | . "github.com/onsi/gomega/ghttp" 12 | "net/http" 13 | ) 14 | 15 | var _ = Describe("DeactivateUser", func() { 16 | BeforeEach(func() { 17 | c := config.NewConfigWithServerURL(server.URL()) 18 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 19 | c.AddContext(ctx) 20 | config.WriteConfig(c) 21 | }) 22 | 23 | It("deactivates a user", func() { 24 | server.RouteToHandler("GET", "/Users", CombineHandlers( 25 | VerifyRequest("GET", "/Users", "filter=userName+eq+%22woodstock@peanuts.com%22&startIndex=1&count=100"), 26 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})), 27 | )) 28 | server.RouteToHandler("PATCH", "/Users/abcdef", CombineHandlers( 29 | VerifyRequest("PATCH", "/Users/abcdef", ""), 30 | VerifyHeaderKV("If-Match", "10"), 31 | VerifyJSON(`{"active": false}`), 32 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com"})), 33 | )) 34 | 35 | session := runCommand("deactivate-user", "woodstock@peanuts.com") 36 | 37 | Expect(server.ReceivedRequests()).To(HaveLen(2)) 38 | Eventually(session).Should(Exit(0)) 39 | Expect(session.Out).To(Say("Account for user woodstock@peanuts.com successfully deactivated.")) 40 | }) 41 | 42 | It("deactivates a user in a non-default zone", func() { 43 | server.RouteToHandler("GET", "/Users", CombineHandlers( 44 | VerifyRequest("GET", "/Users", "filter=userName+eq+%22woodstock@peanuts.com%22&startIndex=1&count=100"), 45 | VerifyHeaderKV("X-Identity-Zone-Id", "twilightzone"), 46 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})), 47 | )) 48 | server.RouteToHandler("PATCH", "/Users/abcdef", CombineHandlers( 49 | VerifyRequest("PATCH", "/Users/abcdef", ""), 50 | VerifyHeaderKV("If-Match", "10"), 51 | VerifyHeaderKV("X-Identity-Zone-Id", "twilightzone"), 52 | VerifyJSON(`{"active": false}`), 53 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com"})), 54 | )) 55 | 56 | session := runCommand("deactivate-user", "woodstock@peanuts.com", "--zone", "twilightzone") 57 | 58 | Expect(server.ReceivedRequests()).To(HaveLen(2)) 59 | Eventually(session).Should(Exit(0)) 60 | Expect(session.Out).To(Say("Account for user woodstock@peanuts.com successfully deactivated.")) 61 | }) 62 | 63 | Describe("validations", func() { 64 | It("requires a target", func() { 65 | config.WriteConfig(config.NewConfig()) 66 | 67 | session := runCommand("deactivate-user", "woodstock@peanuts.com") 68 | 69 | Expect(session.Err).To(Say("You must set a target in order to use this command.")) 70 | Expect(session).Should(Exit(1)) 71 | }) 72 | 73 | It("requires a context", func() { 74 | cfg := config.NewConfigWithServerURL(server.URL()) 75 | config.WriteConfig(cfg) 76 | 77 | session := runCommand("deactivate-user", "woodstock@peanuts.com") 78 | 79 | Expect(session.Err).To(Say("You must have a token in your context to perform this command.")) 80 | Expect(session).Should(Exit(1)) 81 | }) 82 | 83 | It("requires a username", func() { 84 | c := config.NewConfigWithServerURL(server.URL()) 85 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 86 | c.AddContext(ctx) 87 | config.WriteConfig(c) 88 | 89 | session := runCommand("deactivate-user") 90 | 91 | Expect(session.Err).To(Say("The positional argument USERNAME must be specified.")) 92 | Expect(session).Should(Exit(1)) 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /cmd/delete_client.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "code.cloudfoundry.org/uaa-cli/utils" 7 | "github.com/cloudfoundry-community/go-uaa" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func DeleteClientValidations(cfg config.Config, args []string) error { 12 | if err := cli.EnsureContextInConfig(cfg); err != nil { 13 | return err 14 | } 15 | if len(args) == 0 { 16 | return cli.MissingArgumentError("client_id") 17 | } 18 | return nil 19 | } 20 | 21 | func DeleteClientCmd(api *uaa.API, clientId string) error { 22 | _, err := api.DeleteClient(clientId) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | log.Infof("Successfully deleted client %v.", utils.Emphasize(clientId)) 28 | return nil 29 | } 30 | 31 | var deleteClientCmd = &cobra.Command{ 32 | Use: "delete-client CLIENT_ID", 33 | Short: "Delete a client registration", 34 | PreRun: func(cmd *cobra.Command, args []string) { 35 | cfg := GetSavedConfig() 36 | cli.NotifyValidationErrors(DeleteClientValidations(cfg, args), cmd, log) 37 | }, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | api := NewApiFromSavedConfig() 40 | cli.NotifyErrorsWithRetry(DeleteClientCmd(api, args[0]), log, GetSavedConfig()) 41 | }, 42 | } 43 | 44 | func init() { 45 | RootCmd.AddCommand(deleteClientCmd) 46 | deleteClientCmd.Annotations = make(map[string]string) 47 | deleteClientCmd.Annotations[CLIENT_CRUD_CATEGORY] = "true" 48 | deleteClientCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in which to delete the client") 49 | } 50 | -------------------------------------------------------------------------------- /cmd/delete_user.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "code.cloudfoundry.org/uaa-cli/utils" 7 | "errors" 8 | "github.com/cloudfoundry-community/go-uaa" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func DeleteUserCmd(api *uaa.API, username, origin, attributes string) error { 13 | user, err := api.GetUserByUsername(username, origin, attributes) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | _, err = api.DeleteUser(user.ID) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | log.Infof("Account for user %v successfully deleted.", utils.Emphasize(user.Username)) 24 | return nil 25 | } 26 | 27 | func DeleteUserValidations(cfg config.Config, args []string) error { 28 | if err := cli.EnsureContextInConfig(cfg); err != nil { 29 | return err 30 | } 31 | 32 | if len(args) == 0 { 33 | return errors.New("The positional argument USERNAME must be specified.") 34 | } 35 | return nil 36 | } 37 | 38 | var deleteUserCmd = &cobra.Command{ 39 | Use: "delete-user USERNAME", 40 | Short: "Delete a user by username", 41 | PreRun: func(cmd *cobra.Command, args []string) { 42 | err := DeleteUserValidations(GetSavedConfig(), args) 43 | cli.NotifyValidationErrors(err, cmd, log) 44 | }, 45 | Run: func(cmd *cobra.Command, args []string) { 46 | err := DeleteUserCmd(GetAPIFromSavedTokenInContext(), args[0], origin, attributes) 47 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 48 | }, 49 | } 50 | 51 | func init() { 52 | RootCmd.AddCommand(deleteUserCmd) 53 | deleteUserCmd.Annotations = make(map[string]string) 54 | deleteUserCmd.Annotations[USER_CRUD_CATEGORY] = "true" 55 | 56 | deleteUserCmd.Flags().StringVarP(&origin, "origin", "o", "uaa", "user origin") 57 | } 58 | -------------------------------------------------------------------------------- /cmd/delete_user_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "code.cloudfoundry.org/uaa-cli/config" 7 | "code.cloudfoundry.org/uaa-cli/fixtures" 8 | "github.com/cloudfoundry-community/go-uaa" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | . "github.com/onsi/gomega/gbytes" 12 | . "github.com/onsi/gomega/gexec" 13 | . "github.com/onsi/gomega/ghttp" 14 | ) 15 | 16 | var _ = Describe("DeleteUser", func() { 17 | BeforeEach(func() { 18 | c := config.NewConfigWithServerURL(server.URL()) 19 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 20 | c.AddContext(ctx) 21 | Expect(config.WriteConfig(c)).Should(Succeed()) 22 | }) 23 | 24 | It("delete a user", func() { 25 | server.RouteToHandler("GET", "/Users", CombineHandlers( 26 | VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22woodstock%40peanuts.com%22&startIndex=1"), 27 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})), 28 | )) 29 | server.RouteToHandler("DELETE", "/Users/abcdef", CombineHandlers( 30 | VerifyRequest("DELETE", "/Users/abcdef"), 31 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com"})), 32 | )) 33 | 34 | session := runCommand("delete-user", "woodstock@peanuts.com") 35 | 36 | Expect(server.ReceivedRequests()).To(HaveLen(2)) 37 | Eventually(session).Should(Exit(0)) 38 | Expect(session.Out).To(Say("Account for user woodstock@peanuts.com successfully deleted.")) 39 | }) 40 | 41 | Describe("validations", func() { 42 | It("requires a target", func() { 43 | config.WriteConfig(config.NewConfig()) 44 | 45 | session := runCommand("delete-user", "woodstock@peanuts.com") 46 | 47 | Expect(session.Err).To(Say("You must set a target in order to use this command.")) 48 | Expect(session).Should(Exit(1)) 49 | }) 50 | 51 | It("requires a context", func() { 52 | cfg := config.NewConfigWithServerURL(server.URL()) 53 | config.WriteConfig(cfg) 54 | 55 | session := runCommand("delete-user", "woodstock@peanuts.com") 56 | 57 | Expect(session.Err).To(Say("You must have a token in your context to perform this command.")) 58 | Expect(session).Should(Exit(1)) 59 | }) 60 | 61 | It("requires a username", func() { 62 | c := config.NewConfigWithServerURL(server.URL()) 63 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 64 | c.AddContext(ctx) 65 | config.WriteConfig(c) 66 | 67 | session := runCommand("delete-user") 68 | 69 | Expect(session.Err).To(Say("The positional argument USERNAME must be specified.")) 70 | Expect(session).Should(Exit(1)) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /cmd/get_authcode_token.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "github.com/skratchdot/open-golang/open" 7 | "github.com/spf13/cobra" 8 | "golang.org/x/oauth2" 9 | ) 10 | 11 | func addAuthcodeTokenToContext(clientId string, token oauth2.Token, log *cli.Logger) { 12 | ctx := config.UaaContext{ 13 | ClientId: clientId, 14 | Token: token, 15 | GrantType: config.AUTHCODE, 16 | } 17 | 18 | SaveContext(ctx, log) 19 | } 20 | 21 | func AuthcodeTokenArgumentValidation(cfg config.Config, args []string, clientSecret string, tokenFormat string, port int) error { 22 | if err := cli.EnsureTargetInConfig(cfg); err != nil { 23 | return err 24 | } 25 | if len(args) < 1 { 26 | return cli.MissingArgumentError("client_id") 27 | } 28 | if port == 0 { 29 | return cli.MissingArgumentWithExplanationError("port", `The port number must correspond to a localhost redirect_uri specified in the client configuration.`) 30 | } 31 | if clientSecret == "" { 32 | return cli.MissingArgumentError("client_secret") 33 | } 34 | return validateTokenFormatError(tokenFormat) 35 | } 36 | 37 | func AuthcodeTokenCommandRun(doneRunning chan bool, clientId string, authcodeImp cli.ClientImpersonator, log *cli.Logger) { 38 | authcodeImp.Start() 39 | authcodeImp.Authorize() 40 | tokenResponse := <-authcodeImp.Done() 41 | addAuthcodeTokenToContext(clientId, tokenResponse, log) 42 | doneRunning <- true 43 | } 44 | 45 | var getAuthcodeToken = &cobra.Command{ 46 | Use: "get-authcode-token CLIENT_ID -s CLIENT_SECRET --port REDIRECT_URI_PORT", 47 | Short: "Obtain an access token using the authorization_code grant type", 48 | PreRun: func(cmd *cobra.Command, args []string) { 49 | cfg := GetSavedConfig() 50 | cli.NotifyValidationErrors(AuthcodeTokenArgumentValidation(cfg, args, clientSecret, tokenFormat, port), cmd, log) 51 | }, 52 | Run: func(cmd *cobra.Command, args []string) { 53 | done := make(chan bool) 54 | clientId := args[0] 55 | authcodeImp := cli.NewAuthcodeClientImpersonator(GetSavedConfig(), clientId, clientSecret, tokenFormat, scope, port, log, open.Run) 56 | go AuthcodeTokenCommandRun(done, clientId, authcodeImp, GetLogger()) 57 | <-done 58 | }, 59 | } 60 | 61 | func init() { 62 | getAuthcodeToken.Flags().IntVarP(&port, "port", "", 0, "port on which to run local callback server") 63 | getAuthcodeToken.Flags().StringVarP(&clientSecret, "client_secret", "s", "", "client secret") 64 | getAuthcodeToken.Flags().StringVarP(&scope, "scope", "", "openid", "comma-separated scopes to request in token") 65 | getAuthcodeToken.Flags().StringVarP(&tokenFormat, "format", "", "jwt", "available formats include "+availableFormatsStr()) 66 | getAuthcodeToken.Annotations = make(map[string]string) 67 | getAuthcodeToken.Annotations[TOKEN_CATEGORY] = "true" 68 | RootCmd.AddCommand(getAuthcodeToken) 69 | } 70 | -------------------------------------------------------------------------------- /cmd/get_client.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "github.com/cloudfoundry-community/go-uaa" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func GetClientCmd(api *uaa.API, clientId string) error { 11 | client, err := api.GetClient(clientId) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | return cli.NewJsonPrinter(log).Print(client) 17 | } 18 | 19 | func GetClientValidations(cfg config.Config, args []string) error { 20 | if err := cli.EnsureContextInConfig(cfg); err != nil { 21 | return err 22 | } 23 | if len(args) == 0 { 24 | return cli.MissingArgumentError("client_id") 25 | } 26 | return nil 27 | } 28 | 29 | var getClientCmd = &cobra.Command{ 30 | Use: "get-client CLIENT_ID", 31 | Short: "View client registration", 32 | PreRun: func(cmd *cobra.Command, args []string) { 33 | cfg := GetSavedConfig() 34 | cli.NotifyValidationErrors(GetClientValidations(cfg, args), cmd, log) 35 | }, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | api := NewApiFromSavedConfig() 38 | cli.NotifyErrorsWithRetry(GetClientCmd(api, args[0]), log, GetSavedConfig()) 39 | }, 40 | } 41 | 42 | func init() { 43 | RootCmd.AddCommand(getClientCmd) 44 | getClientCmd.Annotations = make(map[string]string) 45 | getClientCmd.Annotations[CLIENT_CRUD_CATEGORY] = "true" 46 | getClientCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in which to get the client") 47 | } 48 | -------------------------------------------------------------------------------- /cmd/get_client_credentials_token.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | 6 | "code.cloudfoundry.org/uaa-cli/cli" 7 | "code.cloudfoundry.org/uaa-cli/config" 8 | "code.cloudfoundry.org/uaa-cli/help" 9 | "github.com/cloudfoundry-community/go-uaa" 10 | "github.com/pkg/errors" 11 | "github.com/spf13/cobra" 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | func GetClientCredentialsTokenValidations(cfg config.Config, args []string, clientSecret string) error { 16 | if err := cli.EnsureTargetInConfig(cfg); err != nil { 17 | return err 18 | } 19 | if len(args) < 1 { 20 | return cli.MissingArgumentError("client_id") 21 | } 22 | if clientSecret == "" { 23 | return cli.MissingArgumentError("client_secret") 24 | } 25 | return validateTokenFormatError(tokenFormat) 26 | } 27 | 28 | func GetClientCredentialsTokenCmd(cfg config.Config, clientId, clientSecret string) error { 29 | var uaaTokenFormat uaa.TokenFormat 30 | //TODO: place in a method to determine uaaTokenFormat 31 | if uaa.JSONWebToken.String() == tokenFormat { 32 | uaaTokenFormat = uaa.JSONWebToken 33 | } else { 34 | uaaTokenFormat = uaa.OpaqueToken 35 | } 36 | 37 | api, err := uaa.New( 38 | cfg.GetActiveTarget().BaseUrl, 39 | uaa.WithClientCredentials( 40 | clientId, 41 | clientSecret, 42 | uaaTokenFormat, 43 | ), 44 | uaa.WithZoneID(cfg.ZoneSubdomain), 45 | uaa.WithSkipSSLValidation(cfg.GetActiveTarget().SkipSSLValidation), 46 | uaa.WithVerbosity(verbose), 47 | ) 48 | if err != nil { 49 | return errors.Wrap(err, "An error occurred while building API with client credentials.") 50 | } 51 | 52 | token, err := api.Token(context.Background()) 53 | if err != nil { 54 | oauthErrorResponse, isRetrieveError := err.(*oauth2.RetrieveError) 55 | if isRetrieveError { 56 | url := oauthErrorResponse.Response.Request.URL.String() 57 | return uaa.RequestError{Url: url, ErrorResponse: oauthErrorResponse.Body} 58 | } else { 59 | return errors.Wrap(err, "An error occurred while fetching token.") 60 | } 61 | } 62 | 63 | activeContext := cfg.GetActiveContext() 64 | activeContext.GrantType = config.CLIENT_CREDENTIALS 65 | activeContext.ClientId = clientId 66 | activeContext.Token = *token 67 | 68 | cfg.AddContext(activeContext) 69 | config.WriteConfig(cfg) 70 | log.Info("Access token successfully fetched and added to context.") 71 | return nil 72 | } 73 | 74 | var getClientCredentialsTokenCmd = &cobra.Command{ 75 | Use: "get-client-credentials-token CLIENT_ID -s CLIENT_SECRET", 76 | Short: "Obtain an access token using the client_credentials grant type", 77 | Long: help.ClientCredentials(), 78 | PreRun: func(cmd *cobra.Command, args []string) { 79 | cfg := GetSavedConfig() 80 | cli.NotifyValidationErrors(GetClientCredentialsTokenValidations(cfg, args, clientSecret), cmd, log) 81 | }, 82 | Run: func(cmd *cobra.Command, args []string) { 83 | cfg := GetSavedConfig() 84 | cli.NotifyErrorsWithRetry(GetClientCredentialsTokenCmd(cfg, args[0], clientSecret), log, GetSavedConfig()) 85 | }, 86 | } 87 | 88 | func init() { 89 | RootCmd.AddCommand(getClientCredentialsTokenCmd) 90 | getClientCredentialsTokenCmd.Flags().StringVarP(&clientSecret, "client_secret", "s", "", "client secret") 91 | getClientCredentialsTokenCmd.Flags().StringVarP(&tokenFormat, "format", "", "jwt", "available formats include "+availableFormatsStr()) 92 | getClientCredentialsTokenCmd.Annotations = make(map[string]string) 93 | getClientCredentialsTokenCmd.Annotations[TOKEN_CATEGORY] = "true" 94 | } 95 | -------------------------------------------------------------------------------- /cmd/get_client_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/config" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | . "github.com/onsi/gomega/gbytes" 8 | . "github.com/onsi/gomega/gexec" 9 | . "github.com/onsi/gomega/ghttp" 10 | "net/http" 11 | ) 12 | 13 | var _ = Describe("GetClient", func() { 14 | const GetClientResponseJson string = `{ 15 | "scope" : [ "clients.read", "clients.write" ], 16 | "client_id" : "clientid", 17 | "resource_ids" : [ "none" ], 18 | "authorized_grant_types" : [ "client_credentials" ], 19 | "redirect_uri" : [ "http://ant.path.wildcard/**/passback/*", "http://test1.com" ], 20 | "authorities" : [ "clients.read", "clients.write" ], 21 | "token_salt" : "1SztLL", 22 | "allowedproviders" : [ "uaa", "ldap", "my-saml-provider" ], 23 | "name" : "My Client Name", 24 | "lastModified" : 1502816030525, 25 | "required_user_groups" : [ "cloud_controller.admin" ] 26 | }` 27 | 28 | Describe("zone switching support", func() { 29 | BeforeEach(func() { 30 | c := config.NewConfigWithServerURL(server.URL()) 31 | c.AddContext(config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 32 | config.WriteConfig(c) 33 | }) 34 | 35 | It("adds the zone switching header", func() { 36 | server.RouteToHandler("GET", "/oauth/clients/clientid", 37 | CombineHandlers( 38 | VerifyRequest("GET", "/oauth/clients/clientid"), 39 | RespondWith(http.StatusOK, GetClientResponseJson, contentTypeJson), 40 | VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"), 41 | VerifyHeaderKV("X-Identity-Zone-Id", "twilight-zone"), 42 | ), 43 | ) 44 | 45 | session := runCommand("get-client", "clientid", "--zone", "twilight-zone") 46 | Eventually(session).Should(Exit(0)) 47 | }) 48 | }) 49 | 50 | Describe("and a target was previously set", func() { 51 | BeforeEach(func() { 52 | c := config.NewConfigWithServerURL(server.URL()) 53 | c.AddContext(config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 54 | config.WriteConfig(c) 55 | }) 56 | 57 | It("shows the client configuration response", func() { 58 | server.RouteToHandler("GET", "/oauth/clients/clientid", 59 | CombineHandlers( 60 | RespondWith(http.StatusOK, GetClientResponseJson, contentTypeJson), 61 | VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"), 62 | ), 63 | ) 64 | 65 | session := runCommand("get-client", "clientid") 66 | 67 | outputBytes := session.Out.Contents() 68 | Expect(outputBytes).To(MatchJSON(GetClientResponseJson)) 69 | Eventually(session).Should(Exit(0)) 70 | }) 71 | 72 | It("handles request errors", func() { 73 | server.RouteToHandler("GET", "/oauth/clients/clientid", 74 | RespondWith(http.StatusNotFound, ""), 75 | ) 76 | 77 | session := runCommand("get-client", "clientid") 78 | 79 | Expect(session.Err).To(Say("An error occurred while calling " + server.URL() + "/oauth/clients/clientid")) 80 | Eventually(session).Should(Exit(1)) 81 | }) 82 | }) 83 | 84 | Describe("when no client_id is supplied", func() { 85 | It("displays an error message to the user", func() { 86 | c := config.NewConfigWithServerURL(server.URL()) 87 | c.AddContext(config.NewContextWithToken("sometoken")) 88 | config.WriteConfig(c) 89 | session := runCommand("get-client") 90 | 91 | Expect(session.Err).To(Say("Missing argument `client_id` must be specified.")) 92 | Eventually(session).Should(Exit(1)) 93 | }) 94 | }) 95 | 96 | Describe("when no target was previously set", func() { 97 | BeforeEach(func() { 98 | c := config.Config{} 99 | config.WriteConfig(c) 100 | }) 101 | 102 | It("tells the user to set a target", func() { 103 | session := runCommand("get-client", "clientid") 104 | 105 | Eventually(session).Should(Exit(1)) 106 | Expect(session.Err).To(Say("You must set a target in order to use this command.")) 107 | }) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /cmd/get_group.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/uaa-cli/cli" 7 | "code.cloudfoundry.org/uaa-cli/config" 8 | "github.com/cloudfoundry-community/go-uaa" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func GetGroupCmd(api *uaa.API, printer cli.Printer, name, attributes string) error { 13 | group, err := api.GetGroupByName(name, attributes) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | return printer.Print(group) 19 | } 20 | 21 | func GetGroupValidations(cfg config.Config, args []string) error { 22 | if err := cli.EnsureContextInConfig(cfg); err != nil { 23 | return err 24 | } 25 | 26 | if len(args) == 0 { 27 | return errors.New("The positional argument GROUPNAME must be specified.") 28 | } 29 | return nil 30 | } 31 | 32 | var getGroupCmd = &cobra.Command{ 33 | Use: "get-group GROUPNAME", 34 | Short: "Look up a group by group name", 35 | PreRun: func(cmd *cobra.Command, args []string) { 36 | cli.NotifyValidationErrors(GetGroupValidations(GetSavedConfig(), args), cmd, log) 37 | }, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | err := GetGroupCmd(GetAPIFromSavedTokenInContext(), cli.NewJsonPrinter(log), args[0], attributes) 40 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 41 | }, 42 | } 43 | 44 | func init() { 45 | RootCmd.AddCommand(getGroupCmd) 46 | getGroupCmd.Annotations = make(map[string]string) 47 | getGroupCmd.Annotations[GROUP_CRUD_CATEGORY] = "true" 48 | 49 | getGroupCmd.Flags().StringVarP(&attributes, "attributes", "a", "", `include only these comma-separated user attributes to improve query performance`) 50 | getGroupCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in which to get the group") 51 | } 52 | -------------------------------------------------------------------------------- /cmd/get_group_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | . "code.cloudfoundry.org/uaa-cli/cmd" 5 | 6 | "code.cloudfoundry.org/uaa-cli/config" 7 | "code.cloudfoundry.org/uaa-cli/fixtures" 8 | "github.com/cloudfoundry-community/go-uaa" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | . "github.com/onsi/gomega/gbytes" 12 | . "github.com/onsi/gomega/gexec" 13 | . "github.com/onsi/gomega/ghttp" 14 | "net/http" 15 | ) 16 | 17 | var _ = Describe("GetGroup", func() { 18 | BeforeEach(func() { 19 | c := config.NewConfigWithServerURL(server.URL()) 20 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 21 | c.AddContext(ctx) 22 | config.WriteConfig(c) 23 | }) 24 | 25 | It("looks up a group with a SCIM filter", func() { 26 | server.RouteToHandler("GET", "/Groups", CombineHandlers( 27 | VerifyRequest("GET", "/Groups", "filter=displayName+eq+%22admin%22&startIndex=1&count=100"), 28 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.Group{DisplayName: "admin"})), 29 | )) 30 | 31 | session := runCommand("get-group", "admin") 32 | 33 | Eventually(session).Should(Say(`"displayName": "admin"`)) 34 | Eventually(session).Should(Exit(0)) 35 | }) 36 | 37 | It("can limit results data with --attributes", func() { 38 | server.RouteToHandler("GET", "/Groups", CombineHandlers( 39 | VerifyRequest("GET", "/Groups", "filter=displayName+eq+%22admin%22&attributes=displayName&startIndex=1&count=100"), 40 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.Group{DisplayName: "admin"})), 41 | )) 42 | 43 | session := runCommand("get-group", "admin", "--attributes", "displayName") 44 | 45 | Eventually(session).Should(Say(`"displayName": "admin"`)) 46 | Eventually(session).Should(Exit(0)) 47 | }) 48 | 49 | It("can understand the --zone flag", func() { 50 | server.RouteToHandler("GET", "/Groups", CombineHandlers( 51 | VerifyRequest("GET", "/Groups", "filter=displayName+eq+%22admin%22&startIndex=1&count=100"), 52 | VerifyHeaderKV("X-Identity-Zone-Id", "twilight-zone"), 53 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.Group{DisplayName: "admin"})), 54 | )) 55 | 56 | session := runCommand("get-group", "admin", "--zone", "twilight-zone") 57 | 58 | Eventually(session).Should(Say(`"displayName": "admin"`)) 59 | Eventually(session).Should(Exit(0)) 60 | }) 61 | 62 | Describe("validations", func() { 63 | It("requires a target", func() { 64 | err := GetGroupValidations(config.Config{}, []string{}) 65 | Expect(err).To(HaveOccurred()) 66 | Expect(err.Error()).To(Equal("You must set a target in order to use this command.")) 67 | }) 68 | 69 | It("requires a context", func() { 70 | cfg := config.NewConfigWithServerURL("http://localhost:9090") 71 | 72 | err := GetGroupValidations(cfg, []string{}) 73 | Expect(err).To(HaveOccurred()) 74 | Expect(err.Error()).To(Equal("You must have a token in your context to perform this command.")) 75 | }) 76 | 77 | It("requires a groupname", func() { 78 | cfg := config.NewConfigWithServerURL("http://localhost:9090") 79 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 80 | cfg.AddContext(ctx) 81 | 82 | err := GetGroupValidations(cfg, []string{}) 83 | Expect(err).To(HaveOccurred()) 84 | Expect(err.Error()).To(Equal("The positional argument GROUPNAME must be specified.")) 85 | 86 | err = GetGroupValidations(cfg, []string{"groupid"}) 87 | Expect(err).NotTo(HaveOccurred()) 88 | }) 89 | }) 90 | 91 | }) 92 | -------------------------------------------------------------------------------- /cmd/get_implicit_token.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "code.cloudfoundry.org/uaa-cli/help" 7 | "github.com/skratchdot/open-golang/open" 8 | "github.com/spf13/cobra" 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | func SaveContext(ctx config.UaaContext, log *cli.Logger) { 13 | c := GetSavedConfig() 14 | c.AddContext(ctx) 15 | config.WriteConfig(c) 16 | log.Info("Access token added to active context.") 17 | } 18 | 19 | func addImplicitTokenToContext(clientId string, token oauth2.Token, log *cli.Logger) { 20 | ctx := config.UaaContext{ 21 | GrantType: config.IMPLICIT, 22 | ClientId: clientId, 23 | Token: token, 24 | } 25 | 26 | SaveContext(ctx, log) 27 | } 28 | 29 | func ImplicitTokenArgumentValidation(cfg config.Config, args []string, port int) error { 30 | if err := cli.EnsureTargetInConfig(cfg); err != nil { 31 | return err 32 | } 33 | if len(args) < 1 { 34 | return cli.MissingArgumentError("client_id") 35 | } 36 | if port == 0 { 37 | return cli.MissingArgumentError("port") 38 | } 39 | return validateTokenFormatError(tokenFormat) 40 | } 41 | 42 | func ImplicitTokenCommandRun(doneRunning chan bool, clientId string, implicitImp cli.ClientImpersonator, log *cli.Logger) { 43 | implicitImp.Start() 44 | implicitImp.Authorize() 45 | tokenResponse := <-implicitImp.Done() 46 | addImplicitTokenToContext(clientId, tokenResponse, log) 47 | doneRunning <- true 48 | } 49 | 50 | var getImplicitToken = &cobra.Command{ 51 | Use: "get-implicit-token CLIENT_ID --port REDIRECT_URI_PORT", 52 | Short: "Obtain an access token using the implicit grant type", 53 | Long: help.ImplicitGrant(), 54 | PreRun: func(cmd *cobra.Command, args []string) { 55 | cfg := GetSavedConfig() 56 | cli.NotifyValidationErrors(ImplicitTokenArgumentValidation(cfg, args, port), cmd, log) 57 | }, 58 | Run: func(cmd *cobra.Command, args []string) { 59 | done := make(chan bool) 60 | baseUrl := GetSavedConfig().GetActiveTarget().BaseUrl 61 | implicitImp := cli.NewImplicitClientImpersonator(args[0], baseUrl, tokenFormat, scope, port, log, open.Run) 62 | go ImplicitTokenCommandRun(done, args[0], implicitImp, GetLogger()) 63 | <-done 64 | }, 65 | } 66 | 67 | func init() { 68 | getImplicitToken.Flags().IntVarP(&port, "port", "", 0, "port on which to run local callback server") 69 | getImplicitToken.Flags().StringVarP(&scope, "scope", "", "openid", "comma-separated scopes to request in token") 70 | getImplicitToken.Flags().StringVarP(&tokenFormat, "format", "", "jwt", "available formats include "+availableFormatsStr()) 71 | getImplicitToken.Annotations = make(map[string]string) 72 | getImplicitToken.Annotations[TOKEN_CATEGORY] = "true" 73 | RootCmd.AddCommand(getImplicitToken) 74 | } 75 | -------------------------------------------------------------------------------- /cmd/get_implicit_token_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | . "code.cloudfoundry.org/uaa-cli/cmd" 5 | 6 | "net/http" 7 | 8 | "code.cloudfoundry.org/uaa-cli/cli" 9 | "code.cloudfoundry.org/uaa-cli/config" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gstruct" 13 | ) 14 | 15 | type TestLauncher struct { 16 | Target string 17 | } 18 | 19 | func (tl *TestLauncher) Run(target string) error { 20 | tl.Target = target 21 | return nil 22 | } 23 | 24 | var _ = Describe("GetImplicitToken", func() { 25 | var c config.Config 26 | var logger cli.Logger 27 | 28 | BeforeEach(func() { 29 | c = config.NewConfigWithServerURL(server.URL()) 30 | config.WriteConfig(c) 31 | logger = cli.NewLogger(GinkgoWriter, GinkgoWriter, GinkgoWriter, GinkgoWriter) 32 | }) 33 | 34 | It("launches a browser for the authorize page and gets the callback params", func() { 35 | launcher := TestLauncher{} 36 | doneRunning := make(chan bool) 37 | 38 | imp := cli.NewImplicitClientImpersonator("shinyclient", server.URL(), "jwt", "openid", 9090, logger, launcher.Run) 39 | go ImplicitTokenCommandRun(doneRunning, "shinyclient", imp, &logger) 40 | 41 | httpClient := &http.Client{} 42 | // UAA sends the user to this redirect_uri after they auth and grant approvals 43 | Eventually(func() (*http.Response, error) { 44 | return httpClient.Get("http://localhost:9090/?access_token=foo&scope=openid&token_type=bearer") 45 | }, AuthCallbackTimeout, AuthCallbackPollInterval).Should(gstruct.PointTo(gstruct.MatchFields( 46 | gstruct.IgnoreExtras, gstruct.Fields{ 47 | "StatusCode": Equal(200), 48 | "Body": Not(BeNil()), 49 | }, 50 | ))) 51 | 52 | Eventually(doneRunning, AuthCallbackTimeout, AuthCallbackPollInterval).Should(Receive()) 53 | 54 | Expect(launcher.Target).To(Equal(server.URL() + "/oauth/authorize?client_id=shinyclient&redirect_uri=http%3A%2F%2Flocalhost%3A9090&response_type=token&scope=openid&token_format=jwt")) 55 | Expect(GetSavedConfig().GetActiveContext().Token.AccessToken).To(Equal("foo")) 56 | Expect(GetSavedConfig().GetActiveContext().Token.TokenType).To(Equal("bearer")) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /cmd/get_password_token.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "code.cloudfoundry.org/uaa-cli/cli" 8 | "code.cloudfoundry.org/uaa-cli/config" 9 | "code.cloudfoundry.org/uaa-cli/help" 10 | "github.com/cloudfoundry-community/go-uaa" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func GetPasswordTokenValidations(cfg config.Config, args []string, username, password string) error { 15 | if err := cli.EnsureTargetInConfig(cfg); err != nil { 16 | return err 17 | } 18 | if len(args) < 1 { 19 | return cli.MissingArgumentError("client_id") 20 | } 21 | if password == "" { 22 | return cli.MissingArgumentError("password") 23 | } 24 | if username == "" { 25 | return cli.MissingArgumentError("username") 26 | } 27 | return validateTokenFormatError(tokenFormat) 28 | } 29 | 30 | func GetPasswordTokenCmd(cfg config.Config, clientId, clientSecret, username, password, tokenFormat string) error { 31 | requestedType := uaa.OpaqueToken 32 | if tokenFormat == uaa.JSONWebToken.String() { 33 | requestedType = uaa.JSONWebToken 34 | } 35 | 36 | api, err := uaa.New( 37 | cfg.GetActiveTarget().BaseUrl, 38 | uaa.WithPasswordCredentials( 39 | clientId, 40 | clientSecret, 41 | username, 42 | password, 43 | requestedType, 44 | ), 45 | uaa.WithZoneID(cfg.ZoneSubdomain), 46 | uaa.WithSkipSSLValidation(cfg.GetActiveTarget().SkipSSLValidation), 47 | uaa.WithVerbosity(verbose), 48 | ) 49 | if err != nil { 50 | return errors.New("An error occurred while fetching token.") 51 | } 52 | 53 | token, err := api.Token(context.Background()) 54 | if err != nil { 55 | log.Info("Unable to retrieve token") 56 | return err 57 | } 58 | 59 | activeContext := cfg.GetActiveContext() 60 | activeContext.ClientId = clientId 61 | activeContext.GrantType = config.PASSWORD 62 | activeContext.Username = username 63 | 64 | activeContext.Token = *token 65 | cfg.AddContext(activeContext) 66 | config.WriteConfig(cfg) 67 | log.Info("Access token successfully fetched and added to context.") 68 | return nil 69 | } 70 | 71 | var getPasswordToken = &cobra.Command{ 72 | Use: "get-password-token CLIENT_ID -s CLIENT_SECRET -u USERNAME -p PASSWORD", 73 | Short: "Obtain an access token using the password grant type", 74 | Long: help.PasswordGrant(), 75 | PreRun: func(cmd *cobra.Command, args []string) { 76 | cfg := GetSavedConfig() 77 | cli.NotifyValidationErrors(GetPasswordTokenValidations(cfg, args, username, password), cmd, log) 78 | }, 79 | Run: func(cmd *cobra.Command, args []string) { 80 | cfg := GetSavedConfig() 81 | clientId := args[0] 82 | cli.NotifyErrorsWithRetry(GetPasswordTokenCmd(cfg, clientId, clientSecret, username, password, tokenFormat), log, GetSavedConfig()) 83 | }, 84 | } 85 | 86 | func init() { 87 | RootCmd.AddCommand(getPasswordToken) 88 | getPasswordToken.Annotations = make(map[string]string) 89 | getPasswordToken.Annotations[TOKEN_CATEGORY] = "true" 90 | getPasswordToken.Flags().StringVarP(&clientSecret, "client_secret", "s", "", "client secret") 91 | getPasswordToken.Flags().StringVarP(&username, "username", "u", "", "username") 92 | getPasswordToken.Flags().StringVarP(&password, "password", "p", "", "user password") 93 | getPasswordToken.Flags().StringVarP(&tokenFormat, "format", "", "jwt", "available formats include "+availableFormatsStr()) 94 | } 95 | -------------------------------------------------------------------------------- /cmd/get_token_key.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "github.com/cloudfoundry-community/go-uaa" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func GetTokenKeyCmd(api *uaa.API) error { 10 | key, err := api.TokenKey() 11 | if err != nil { 12 | return err 13 | } 14 | 15 | return cli.NewJsonPrinter(log).Print(key) 16 | } 17 | 18 | var getTokenKeyCmd = &cobra.Command{ 19 | Use: "get-token-key", 20 | Short: "View the key for validating UAA's JWT token signatures", 21 | Aliases: []string{"token-key"}, 22 | PreRun: func(cmd *cobra.Command, args []string) { 23 | cfg := GetSavedConfig() 24 | cli.NotifyValidationErrors(cli.EnsureTargetInConfig(cfg), cmd, log) 25 | }, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | cli.NotifyErrorsWithRetry(GetTokenKeyCmd(GetUnauthenticatedAPI()), log, GetSavedConfig()) 28 | }, 29 | } 30 | 31 | func init() { 32 | RootCmd.AddCommand(getTokenKeyCmd) 33 | getTokenKeyCmd.Annotations = make(map[string]string) 34 | getTokenKeyCmd.Annotations[TOKEN_CATEGORY] = "true" 35 | } 36 | -------------------------------------------------------------------------------- /cmd/get_token_key_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | . "github.com/onsi/gomega/gbytes" 9 | . "github.com/onsi/gomega/gexec" 10 | . "github.com/onsi/gomega/ghttp" 11 | "net/http" 12 | ) 13 | 14 | var _ = Describe("GetTokenKey", func() { 15 | Describe("and a target was previously set", func() { 16 | BeforeEach(func() { 17 | c := config.NewConfigWithServerURL(server.URL()) 18 | config.WriteConfig(c) 19 | }) 20 | 21 | It("shows the TokenKey response", func() { 22 | asymmetricKeyResponse := `{ 23 | "kty" : "RSA", 24 | "e" : "AQAB", 25 | "use" : "sig", 26 | "kid" : "testKey", 27 | "alg" : "RS256", 28 | "value" : "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0m59l2u9iDnMbrXHfqkO\nrn2dVQ3vfBJqcDuFUK03d+1PZGbVlNCqnkpIJ8syFppW8ljnWweP7+LiWpRoz0I7\nfYb3d8TjhV86Y997Fl4DBrxgM6KTJOuE/uxnoDhZQ14LgOU2ckXjOzOdTsnGMKQB\nLCl0vpcXBtFLMaSbpv1ozi8h7DJyVZ6EnFQZUWGdgTMhDrmqevfx95U/16c5WBDO\nkqwIn7Glry9n9Suxygbf8g5AzpWcusZgDLIIZ7JTUldBb8qU2a0Dl4mvLZOn4wPo\njfj9Cw2QICsc5+Pwf21fP+hzf+1WSRHbnYv8uanRO0gZ8ekGaghM/2H6gqJbo2nI\nJwIDAQAB\n-----END PUBLIC KEY-----", 29 | "n" : "ANJufZdrvYg5zG61x36pDq59nVUN73wSanA7hVCtN3ftT2Rm1ZTQqp5KSCfLMhaaVvJY51sHj-_i4lqUaM9CO32G93fE44VfOmPfexZeAwa8YDOikyTrhP7sZ6A4WUNeC4DlNnJF4zsznU7JxjCkASwpdL6XFwbRSzGkm6b9aM4vIewyclWehJxUGVFhnYEzIQ65qnr38feVP9enOVgQzpKsCJ-xpa8vZ_UrscoG3_IOQM6VnLrGYAyyCGeyU1JXQW_KlNmtA5eJry2Tp-MD6I34_QsNkCArHOfj8H9tXz_oc3_tVkkR252L_Lmp0TtIGfHpBmoITP9h-oKiW6NpyCc" 30 | }` 31 | 32 | server.RouteToHandler("GET", "/token_key", 33 | CombineHandlers( 34 | RespondWith(http.StatusOK, asymmetricKeyResponse, contentTypeJson), 35 | ), 36 | ) 37 | 38 | session := runCommand("get-token-key") 39 | 40 | outputBytes := session.Out.Contents() 41 | Expect(outputBytes).To(MatchJSON(asymmetricKeyResponse)) 42 | Expect(session).Should(Exit(0)) 43 | }) 44 | 45 | It("shows symmetric keys with unused JWK fields omitted", func() { 46 | symmetricKeyResponse := `{ 47 | "kty" : "MAC", 48 | "alg" : "HS256", 49 | "value" : "key", 50 | "use" : "sig", 51 | "kid" : "testKey" 52 | }` 53 | 54 | server.RouteToHandler("GET", "/token_key", 55 | CombineHandlers( 56 | RespondWith(http.StatusOK, symmetricKeyResponse, contentTypeJson), 57 | ), 58 | ) 59 | 60 | session := runCommand("get-token-key") 61 | 62 | outputBytes := session.Out.Contents() 63 | Expect(outputBytes).To(MatchJSON(symmetricKeyResponse)) 64 | Expect(session).Should(Exit(0)) 65 | }) 66 | }) 67 | 68 | Describe("Validations", func() { 69 | It("it requires a target to have been set", func() { 70 | config.WriteConfig(config.NewConfig()) 71 | 72 | session := runCommand("get-token-key") 73 | 74 | Eventually(session).Should(Exit(1)) 75 | Expect(session.Err).To(Say(cli.MISSING_TARGET)) 76 | }) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /cmd/get_token_keys.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "github.com/cloudfoundry-community/go-uaa" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func GetTokenKeysCmd(api *uaa.API) error { 10 | key, err := api.TokenKeys() 11 | 12 | if err != nil { 13 | return err 14 | } 15 | 16 | return cli.NewJsonPrinter(log).Print(key) 17 | } 18 | 19 | var getTokenKeysCmd = &cobra.Command{ 20 | Use: "get-token-keys", 21 | Short: "View all keys the UAA has used to sign JWT tokens", 22 | Aliases: []string{"token-keys"}, 23 | PreRun: func(cmd *cobra.Command, args []string) { 24 | cfg := GetSavedConfig() 25 | cli.NotifyValidationErrors(cli.EnsureTargetInConfig(cfg), cmd, log) 26 | }, 27 | Run: func(cmd *cobra.Command, args []string) { 28 | cli.NotifyErrorsWithRetry(GetTokenKeysCmd(GetUnauthenticatedAPI()), log, GetSavedConfig()) 29 | }, 30 | } 31 | 32 | func init() { 33 | RootCmd.AddCommand(getTokenKeysCmd) 34 | getTokenKeysCmd.Annotations = make(map[string]string) 35 | getTokenKeysCmd.Annotations[TOKEN_CATEGORY] = "true" 36 | } 37 | -------------------------------------------------------------------------------- /cmd/get_user.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "errors" 7 | "github.com/cloudfoundry-community/go-uaa" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func GetUserCmd(api *uaa.API, printer cli.Printer, username, origin, attributes string) error { 12 | user, err := api.GetUserByUsername(username, origin, attributes) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | return printer.Print(user) 18 | } 19 | 20 | func GetUserValidations(cfg config.Config, args []string) error { 21 | if err := cli.EnsureContextInConfig(cfg); err != nil { 22 | return err 23 | } 24 | 25 | if len(args) == 0 { 26 | return errors.New("The positional argument USERNAME must be specified.") 27 | } 28 | return nil 29 | } 30 | 31 | var getUserCmd = &cobra.Command{ 32 | Use: "get-user USERNAME", 33 | Short: "Look up a user by username", 34 | PreRun: func(cmd *cobra.Command, args []string) { 35 | cli.NotifyValidationErrors(GetUserValidations(GetSavedConfig(), args), cmd, log) 36 | }, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | err := GetUserCmd(GetAPIFromSavedTokenInContext(), cli.NewJsonPrinter(log), args[0], origin, attributes) 39 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 40 | }, 41 | } 42 | 43 | func init() { 44 | RootCmd.AddCommand(getUserCmd) 45 | getUserCmd.Annotations = make(map[string]string) 46 | getUserCmd.Annotations[USER_CRUD_CATEGORY] = "true" 47 | 48 | getUserCmd.Flags().StringVarP(&origin, "origin", "o", "", `The identity provider in which to search. Examples: uaa, ldap, etc. `) 49 | getUserCmd.Flags().StringVarP(&attributes, "attributes", "a", "", `include only these comma-separated user attributes to improve query performance`) 50 | getUserCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in find the user") 51 | } 52 | -------------------------------------------------------------------------------- /cmd/get_user_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | . "code.cloudfoundry.org/uaa-cli/cmd" 5 | 6 | "code.cloudfoundry.org/uaa-cli/config" 7 | "code.cloudfoundry.org/uaa-cli/fixtures" 8 | "github.com/cloudfoundry-community/go-uaa" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | . "github.com/onsi/gomega/gbytes" 12 | . "github.com/onsi/gomega/gexec" 13 | . "github.com/onsi/gomega/ghttp" 14 | "net/http" 15 | ) 16 | 17 | var _ = Describe("GetUser", func() { 18 | BeforeEach(func() { 19 | c := config.NewConfigWithServerURL(server.URL()) 20 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 21 | c.AddContext(ctx) 22 | config.WriteConfig(c) 23 | }) 24 | 25 | It("looks up a user with a SCIM filter", func() { 26 | server.RouteToHandler("GET", "/Users", CombineHandlers( 27 | VerifyRequest("GET", "/Users", "filter=userName+eq+%22woodstock@peanuts.com%22+and+origin+eq+%22uaa%22&startIndex=1&count=100"), 28 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com"})), 29 | )) 30 | 31 | session := runCommand("get-user", "woodstock@peanuts.com", "--origin", "uaa") 32 | 33 | Eventually(session).Should(Say(`"userName": "woodstock@peanuts.com"`)) 34 | Eventually(session).Should(Exit(0)) 35 | }) 36 | 37 | It("can understand the --zone flag", func() { 38 | server.RouteToHandler("GET", "/Users", CombineHandlers( 39 | VerifyRequest("GET", "/Users", "filter=userName+eq+%22woodstock@peanuts.com%22&startIndex=1&count=100"), 40 | VerifyHeaderKV("X-Identity-Zone-Id", "twilight-zone"), 41 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com"})), 42 | )) 43 | 44 | session := runCommand("get-user", "woodstock@peanuts.com", "--zone", "twilight-zone") 45 | 46 | Eventually(session).Should(Say(`"userName": "woodstock@peanuts.com"`)) 47 | Eventually(session).Should(Exit(0)) 48 | }) 49 | 50 | It("can limit results data with --attributes", func() { 51 | server.RouteToHandler("GET", "/Users", CombineHandlers( 52 | VerifyRequest("GET", "/Users", "filter=userName+eq+%22woodstock@peanuts.com%22+and+origin+eq+%22uaa%22&attributes=userName&startIndex=1&count=100"), 53 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com"})), 54 | )) 55 | 56 | session := runCommand("get-user", "woodstock@peanuts.com", "--origin", "uaa", "--attributes", "userName") 57 | 58 | Eventually(session).Should(Say(`"userName": "woodstock@peanuts.com"`)) 59 | Eventually(session).Should(Exit(0)) 60 | }) 61 | 62 | Describe("validations", func() { 63 | It("requires a target", func() { 64 | err := GetUserValidations(config.Config{}, []string{}) 65 | Expect(err).To(HaveOccurred()) 66 | Expect(err.Error()).To(Equal("You must set a target in order to use this command.")) 67 | }) 68 | 69 | It("requires a context", func() { 70 | cfg := config.NewConfigWithServerURL("http://localhost:9090") 71 | 72 | err := GetUserValidations(cfg, []string{}) 73 | Expect(err).To(HaveOccurred()) 74 | Expect(err.Error()).To(Equal("You must have a token in your context to perform this command.")) 75 | }) 76 | 77 | It("requires a username", func() { 78 | cfg := config.NewConfigWithServerURL("http://localhost:9090") 79 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 80 | cfg.AddContext(ctx) 81 | 82 | err := GetUserValidations(cfg, []string{}) 83 | Expect(err).To(HaveOccurred()) 84 | Expect(err.Error()).To(Equal("The positional argument USERNAME must be specified.")) 85 | 86 | err = GetUserValidations(cfg, []string{"userid"}) 87 | Expect(err).NotTo(HaveOccurred()) 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /cmd/group_mapping_validations.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "errors" 7 | ) 8 | 9 | func GroupMappingValidations(cfg config.Config, args []string) error { 10 | if err := cli.EnsureContextInConfig(cfg); err != nil { 11 | return err 12 | } 13 | 14 | if len(args) != 2 { 15 | return errors.New("The positional arguments EXTERNAL_GROUPNAME and GROUPNAME must be specified.") 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /cmd/group_mapping_validations_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cmd" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("validations", func() { 11 | Describe("without a target and context", func() { 12 | It("requires a target", func() { 13 | err := cmd.GroupMappingValidations(config.Config{}, []string{}) 14 | Expect(err).To(HaveOccurred()) 15 | Expect(err.Error()).To(Equal("You must set a target in order to use this command.")) 16 | }) 17 | 18 | It("requires a context", func() { 19 | cfg := config.NewConfigWithServerURL("http://localhost:9090") 20 | 21 | err := cmd.GroupMappingValidations(cfg, []string{}) 22 | Expect(err).To(HaveOccurred()) 23 | Expect(err.Error()).To(Equal("You must have a token in your context to perform this command.")) 24 | }) 25 | }) 26 | 27 | Describe("without required params", func() { 28 | It("requires a external_group_name", func() { 29 | cfg := buildConfig("http://localhost:9090") 30 | 31 | err := cmd.GroupMappingValidations(cfg, []string{}) 32 | Expect(err).To(HaveOccurred()) 33 | Expect(err.Error()).To(Equal("The positional arguments EXTERNAL_GROUPNAME and GROUPNAME must be specified.")) 34 | }) 35 | 36 | It("requires a group_name", func() { 37 | cfg := buildConfig("http://localhost:9090") 38 | 39 | err := cmd.GroupMappingValidations(cfg, []string{"external_group"}) 40 | Expect(err).To(HaveOccurred()) 41 | Expect(err.Error()).To(Equal("The positional arguments EXTERNAL_GROUPNAME and GROUPNAME must be specified.")) 42 | }) 43 | }) 44 | 45 | Describe("with totally valid data", func() { 46 | It("does not complain", func() { 47 | cfg := buildConfig("http://localhost:9090") 48 | 49 | err := cmd.GroupMappingValidations(cfg, []string{"external_groupname", "groupname"}) 50 | Expect(err).NotTo(HaveOccurred()) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /cmd/helpers_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/config" 5 | "code.cloudfoundry.org/uaa-cli/fixtures" 6 | "fmt" 7 | "github.com/cloudfoundry-community/go-uaa" 8 | "net/http" 9 | 10 | . "github.com/onsi/gomega/ghttp" 11 | ) 12 | 13 | var buildConfig = func(target string) config.Config { 14 | cfg := config.NewConfigWithServerURL(target) 15 | ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") 16 | cfg.AddContext(ctx) 17 | 18 | return cfg 19 | } 20 | 21 | var mockGroupLookup = func(id, groupname string) { 22 | server.RouteToHandler("GET", "/Groups", CombineHandlers( 23 | VerifyRequest("GET", "/Groups", fmt.Sprintf("filter=displayName+eq+%%22%s%%22&startIndex=1&count=100", groupname)), 24 | RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.Group{ID: id, DisplayName: groupname})), 25 | )) 26 | } 27 | 28 | var mockExternalGroupMapping = func(externalGroupname, internalGroupId, internalGroupname, origin string) { 29 | server.RouteToHandler("POST", "/Groups/External", CombineHandlers( 30 | VerifyRequest("POST", "/Groups/External"), 31 | VerifyJSONRepresenting(map[string]interface{}{ 32 | "groupId": internalGroupId, 33 | "externalGroup": externalGroupname, 34 | "origin": origin, 35 | }), 36 | RespondWith(http.StatusCreated, fixtures.EntityResponse( 37 | uaa.GroupMapping{ 38 | GroupID: internalGroupId, 39 | ExternalGroup: externalGroupname, 40 | DisplayName: internalGroupname, 41 | Origin: origin, 42 | Schemas: []string{"urn:scim:schemas:core:1.0"}, 43 | })), 44 | )) 45 | } 46 | 47 | var mockExternalGroupUnmapping = func(externalGroupname, internalGroupId, internalGroupname, origin string) { 48 | path := fmt.Sprintf("/Groups/External/groupId/%s/externalGroup/%s/origin/%s", internalGroupId, externalGroupname, origin) 49 | server.RouteToHandler("DELETE", path, CombineHandlers( 50 | VerifyRequest("DELETE", path), 51 | RespondWith(http.StatusOK, fixtures.EntityResponse( 52 | uaa.GroupMapping{ 53 | GroupID: internalGroupId, 54 | ExternalGroup: externalGroupname, 55 | DisplayName: internalGroupname, 56 | Origin: origin, 57 | Schemas: []string{"urn:scim:schemas:core:1.0"}, 58 | })), 59 | )) 60 | } 61 | -------------------------------------------------------------------------------- /cmd/info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "github.com/cloudfoundry-community/go-uaa" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func InfoCmd(api *uaa.API) error { 10 | i, err := api.GetInfo() 11 | if err != nil { 12 | return err 13 | } 14 | 15 | return cli.NewJsonPrinter(log).Print(i) 16 | } 17 | 18 | var infoCmd = &cobra.Command{ 19 | Use: "info", 20 | Short: "See version and global configurations for the targeted UAA", 21 | PreRun: func(cmd *cobra.Command, args []string) { 22 | cfg := GetSavedConfig() 23 | cli.NotifyValidationErrors(cli.EnsureTargetInConfig(cfg), cmd, log) 24 | }, 25 | Run: func(cmd *cobra.Command, args []string) { 26 | cli.NotifyErrorsWithRetry(InfoCmd(GetUnauthenticatedAPI()), log, GetSavedConfig()) 27 | }, 28 | } 29 | 30 | func init() { 31 | RootCmd.AddCommand(infoCmd) 32 | infoCmd.Annotations = make(map[string]string) 33 | infoCmd.Annotations[INTRO_CATEGORY] = "true" 34 | } 35 | -------------------------------------------------------------------------------- /cmd/info_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/config" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | . "github.com/onsi/gomega/gbytes" 8 | . "github.com/onsi/gomega/gexec" 9 | . "github.com/onsi/gomega/ghttp" 10 | "net/http" 11 | ) 12 | 13 | var _ = Describe("Info", func() { 14 | Describe("and a target was previously set", func() { 15 | BeforeEach(func() { 16 | c := config.NewConfigWithServerURL(server.URL()) 17 | err := config.WriteConfig(c) 18 | Expect(err).NotTo(HaveOccurred()) 19 | }) 20 | 21 | It("shows the info response", func() { 22 | server.RouteToHandler("GET", "/info", 23 | RespondWith(http.StatusOK, InfoResponseJson, contentTypeJson), 24 | ) 25 | 26 | session := runCommand("info") 27 | 28 | Eventually(session).Should(Exit(0)) 29 | outputBytes := session.Out.Contents() 30 | Expect(outputBytes).To(MatchJSON(InfoResponseJson)) 31 | }) 32 | 33 | It("handles request errors", func() { 34 | server.RouteToHandler("GET", "/info", 35 | RespondWith(http.StatusBadRequest, ""), 36 | ) 37 | 38 | session := runCommand("info") 39 | 40 | Eventually(session).Should(Exit(1)) 41 | Expect(session.Err).To(Say("An error occurred while calling " + server.URL() + "/info")) 42 | }) 43 | 44 | Context("with --verbose", func() { 45 | ItSupportsTheVerboseFlagWhenGet("info", "/info", InfoResponseJson) 46 | }) 47 | }) 48 | 49 | Describe("when no target was previously set", func() { 50 | BeforeEach(func() { 51 | c := config.Config{} 52 | err := config.WriteConfig(c) 53 | Expect(err).NotTo(HaveOccurred()) 54 | }) 55 | 56 | It("tells the user to set a target", func() { 57 | session := runCommand("info") 58 | 59 | Eventually(session).Should(Exit(1)) 60 | Expect(session.Err).To(Say("You must set a target in order to use this command.")) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /cmd/list_clients.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "github.com/cloudfoundry-community/go-uaa" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func ListClientsValidations(cfg config.Config) error { 11 | if err := cli.EnsureContextInConfig(cfg); err != nil { 12 | return err 13 | } 14 | return nil 15 | } 16 | 17 | func ListClientsCmd(api *uaa.API) error { 18 | clients, err := api.ListAllClients("", "", uaa.SortAscending) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return cli.NewJsonPrinter(log).Print(clients) 24 | } 25 | 26 | var listClientsCmd = &cobra.Command{ 27 | Use: "list-clients", 28 | Short: "See all clients in the targeted UAA", 29 | Aliases: []string{"clients"}, 30 | PreRun: func(cmd *cobra.Command, args []string) { 31 | cli.NotifyValidationErrors(ListClientsValidations(GetSavedConfig()), cmd, log) 32 | }, 33 | Run: func(cmd *cobra.Command, args []string) { 34 | cli.NotifyErrorsWithRetry(ListClientsCmd(GetAPIFromSavedTokenInContext()), log, GetSavedConfig()) 35 | }, 36 | } 37 | 38 | func init() { 39 | RootCmd.AddCommand(listClientsCmd) 40 | listClientsCmd.Annotations = make(map[string]string) 41 | listClientsCmd.Annotations[CLIENT_CRUD_CATEGORY] = "true" 42 | listClientsCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in which to get the client") 43 | } 44 | -------------------------------------------------------------------------------- /cmd/list_clients_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/config" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | . "github.com/onsi/gomega/gbytes" 8 | . "github.com/onsi/gomega/gexec" 9 | . "github.com/onsi/gomega/ghttp" 10 | "net/http" 11 | ) 12 | 13 | var _ = Describe("ListClients", func() { 14 | const ClientsListResponseJsonPage1 = `{ 15 | "resources" : [{ 16 | "client_id" : "client1" 17 | }, 18 | { 19 | "client_id" : "client2" 20 | }], 21 | "startIndex" : 1, 22 | "itemsPerPage" : 2, 23 | "totalResults" : 2, 24 | "schemas" : [ "http://cloudfoundry.org/schema/scim/oauth-clients-1.0" ] 25 | }` 26 | 27 | Describe("zone switching support", func() { 28 | BeforeEach(func() { 29 | c := config.NewConfigWithServerURL(server.URL()) 30 | c.AddContext(config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 31 | config.WriteConfig(c) 32 | }) 33 | 34 | It("adds the zone switching header", func() { 35 | server.RouteToHandler("GET", "/oauth/clients", 36 | CombineHandlers( 37 | VerifyRequest("GET", "/oauth/clients"), 38 | RespondWith(http.StatusOK, ClientsListResponseJsonPage1, contentTypeJson), 39 | VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"), 40 | VerifyHeaderKV("X-Identity-Zone-Id", "twilight-zone"), 41 | ), 42 | ) 43 | 44 | session := runCommand("list-clients", "--verbose", "--zone", "twilight-zone") 45 | Eventually(session).Should(Exit(0)) 46 | }) 47 | }) 48 | 49 | Describe("and a target was previously set", func() { 50 | BeforeEach(func() { 51 | c := config.NewConfigWithServerURL(server.URL()) 52 | c.AddContext(config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 53 | config.WriteConfig(c) 54 | }) 55 | 56 | Context("when the request to /oauth/clients succeeds", func() { 57 | BeforeEach(func() { 58 | server.RouteToHandler("GET", "/oauth/clients", 59 | CombineHandlers( 60 | RespondWith(http.StatusOK, ClientsListResponseJsonPage1, contentTypeJson), 61 | VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"), 62 | ), 63 | ) 64 | }) 65 | 66 | It("shows the client configuration response", func() { 67 | session := runCommand("list-clients") 68 | 69 | outputBytes := session.Out.Contents() 70 | Expect(outputBytes).To(MatchJSON(`[{ "client_id" : "client1" }, { "client_id" : "client2" }]`)) 71 | Eventually(session).Should(Exit(0)) 72 | }) 73 | 74 | It("shows verbose output when passed --verbose", func() { 75 | session := runCommand("list-clients", "--verbose") 76 | 77 | Eventually(session).Should(Exit(0)) 78 | Expect(session.Out).To(Say("GET " + "/oauth/clients")) 79 | Expect(session.Out).To(Say("Accept: application/json")) 80 | Expect(session.Out).To(Say("200 OK")) 81 | }) 82 | 83 | }) 84 | 85 | Context("when the request to /oauth/clients fails", func() { 86 | BeforeEach(func() { 87 | server.RouteToHandler("GET", "/oauth/clients", 88 | RespondWith(http.StatusNotFound, ""), 89 | ) 90 | }) 91 | 92 | It("handles request errors", func() { 93 | session := runCommand("list-clients") 94 | 95 | Expect(session.Err).To(Say("An error occurred while calling " + server.URL() + "/oauth/clients")) 96 | Eventually(session).Should(Exit(1)) 97 | }) 98 | }) 99 | }) 100 | 101 | Describe("when no target was previously set", func() { 102 | BeforeEach(func() { 103 | c := config.Config{} 104 | config.WriteConfig(c) 105 | }) 106 | 107 | It("tells the user to set a target", func() { 108 | session := runCommand("list-clients") 109 | 110 | Eventually(session).Should(Exit(1)) 111 | Expect(session.Err).To(Say("You must set a target in order to use this command.")) 112 | }) 113 | }) 114 | 115 | Describe("when no context was previously set", func() { 116 | BeforeEach(func() { 117 | c := config.NewConfigWithServerURL(server.URL()) 118 | config.WriteConfig(c) 119 | }) 120 | 121 | It("tells the user to set a context", func() { 122 | session := runCommand("list-clients") 123 | 124 | Eventually(session).Should(Exit(1)) 125 | Expect(session.Err).To(Say("You must have a token in your context to perform this command.")) 126 | }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /cmd/list_group_mappings.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "github.com/cloudfoundry-community/go-uaa" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func ListGroupMappingsCmd(api *uaa.API, printer cli.Printer) error { 10 | group, err := api.ListAllGroupMappings("") 11 | if err != nil { 12 | return err 13 | } 14 | return printer.Print(group) 15 | } 16 | 17 | var listGroupMappingsCmd = &cobra.Command{ 18 | Use: "list-group-mappings", 19 | Aliases: []string{}, 20 | Short: "List all the mappings between uaa scopes and external groups", 21 | PreRun: func(cmd *cobra.Command, args []string) { 22 | cli.NotifyValidationErrors(ListGroupValidations(GetSavedConfig()), cmd, log) 23 | }, 24 | Run: func(cmd *cobra.Command, args []string) { 25 | err := ListGroupMappingsCmd(GetAPIFromSavedTokenInContext(), cli.NewJsonPrinter(log)) 26 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 27 | }, 28 | } 29 | 30 | func init() { 31 | RootCmd.AddCommand(listGroupMappingsCmd) 32 | listGroupMappingsCmd.Annotations = make(map[string]string) 33 | listGroupMappingsCmd.Annotations[GROUP_CRUD_CATEGORY] = "true" 34 | } 35 | -------------------------------------------------------------------------------- /cmd/list_group_mappings_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "code.cloudfoundry.org/uaa-cli/config" 7 | . "code.cloudfoundry.org/uaa-cli/fixtures" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | . "github.com/onsi/gomega/gexec" 12 | . "github.com/onsi/gomega/ghttp" 13 | ) 14 | 15 | var _ = Describe("ListGroupMappings", func() { 16 | BeforeEach(func() { 17 | cfg := config.NewConfigWithServerURL(server.URL()) 18 | cfg.AddContext(config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 19 | err := config.WriteConfig(cfg) 20 | Expect(err).NotTo(HaveOccurred()) 21 | }) 22 | Describe("By default", func() { 23 | 24 | It("requests group mappings from the backend with default parameters", func() { 25 | server.RouteToHandler("GET", "/Groups/External", CombineHandlers( 26 | VerifyRequest("GET", "/Groups/External", "startIndex=1&count=100"), 27 | RespondWith(http.StatusOK, ExternalGroupsApiResponse, contentTypeJson), 28 | )) 29 | 30 | session := runCommand("list-group-mappings") 31 | 32 | Eventually(session).Should(Exit(0)) 33 | 34 | // We can't verify that the right JSON was output 35 | // There seems to be a gap in the tooling. 36 | // We can test a regex against a buffer 37 | // We can test JSON against a string 38 | // But we can't test JSON against a buffer 39 | Eventually(session.Out).Should(gbytes.Say("organizations.acme")) 40 | }) 41 | 42 | It("prints a useful description in the help menu", func() { 43 | session := runCommand("list-group-mappings", "-h") 44 | 45 | Eventually(session).Should(Exit(0)) 46 | Eventually(session.Out).Should(gbytes.Say("List all the mappings between uaa scopes and external groups")) 47 | }) 48 | }) 49 | Describe("When the context has insufficient scope", func() { 50 | It("returns an error", func() { 51 | server.RouteToHandler("GET", "/Groups/External", CombineHandlers( 52 | VerifyRequest("GET", "/Groups/External", "startIndex=1&count=100"), 53 | RespondWith(http.StatusForbidden, ExternalGroupsApiResponseInsufficientScope, contentTypeJson), 54 | )) 55 | 56 | session := runCommand("list-group-mappings") 57 | 58 | Eventually(session).Should(Exit(1)) 59 | Eventually(session.Err).Should(gbytes.Say(`An error occurred while calling http://127.0.0.1:(\d+)/Groups/External\?count=100&startIndex=1`)) 60 | Eventually(session.Err).Should(gbytes.Say(`"error": "insufficient_scope"`)) 61 | Eventually(session.Err).Should(gbytes.Say(`"error_description": "Insufficient scope for this resource"`)) 62 | Eventually(session.Err).Should(gbytes.Say(`"scope": "uaa.admin scim.read zones.uaa.admin"`)) 63 | Eventually(session.Out).Should(gbytes.Say("Retry with --verbose for more information.")) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /cmd/list_groups.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "github.com/cloudfoundry-community/go-uaa" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func ListGroupValidations(cfg config.Config) error { 11 | if err := cli.EnsureContextInConfig(cfg); err != nil { 12 | return err 13 | } 14 | return nil 15 | } 16 | 17 | func ListGroupsCmd(api *uaa.API, printer cli.Printer, filter, sortBy, sortOrder, attributes string) error { 18 | group, err := api.ListAllGroups(filter, sortBy, attributes, uaa.SortOrder(sortOrder)) 19 | if err != nil { 20 | return err 21 | } 22 | return printer.Print(group) 23 | } 24 | 25 | var listGroupsCmd = &cobra.Command{ 26 | Use: "list-groups", 27 | Aliases: []string{"groups", "get-groups", "search-groups"}, 28 | Short: "Search and list groups with SCIM filters", 29 | PreRun: func(cmd *cobra.Command, args []string) { 30 | cli.NotifyValidationErrors(ListGroupValidations(GetSavedConfig()), cmd, log) 31 | }, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | err := ListGroupsCmd(GetAPIFromSavedTokenInContext(), cli.NewJsonPrinter(log), filter, sortBy, sortOrder, attributes) 34 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 35 | }, 36 | } 37 | 38 | func init() { 39 | RootCmd.AddCommand(listGroupsCmd) 40 | listGroupsCmd.Annotations = make(map[string]string) 41 | listGroupsCmd.Annotations[GROUP_CRUD_CATEGORY] = "true" 42 | 43 | listGroupsCmd.Flags().StringVarP(&filter, "filter", "", "", `a SCIM filter, or query, e.g. 'id eq "a5e3f9fb-65a0-4033-a86c-11f4712e1fed"'`) 44 | listGroupsCmd.Flags().StringVarP(&sortBy, "sortBy", "b", "", `the attribute to sort results by, e.g. "created" or "displayName"`) 45 | listGroupsCmd.Flags().StringVarP(&sortOrder, "sortOrder", "o", "", `how results should be ordered. One of: [ascending, descending]`) 46 | listGroupsCmd.Flags().StringVarP(&attributes, "attributes", "a", "", `include only these comma-separated attributes to improve query performance`) 47 | listGroupsCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain from which to list the groups") 48 | } 49 | -------------------------------------------------------------------------------- /cmd/list_groups_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "code.cloudfoundry.org/uaa-cli/config" 8 | . "code.cloudfoundry.org/uaa-cli/fixtures" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | . "github.com/onsi/gomega/gexec" 12 | . "github.com/onsi/gomega/ghttp" 13 | ) 14 | 15 | var _ = Describe("ListGroups", func() { 16 | 17 | var groupListResponse string 18 | 19 | BeforeEach(func() { 20 | cfg := config.NewConfigWithServerURL(server.URL()) 21 | cfg.AddContext(config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 22 | config.WriteConfig(cfg) 23 | groupListResponse = fmt.Sprintf(PaginatedResponseTmpl, UaaAdminGroupResponse, CloudControllerReadGroupResponse) 24 | }) 25 | 26 | It("executes SCIM queries based on flags", func() { 27 | server.RouteToHandler("GET", "/Groups", CombineHandlers( 28 | VerifyRequest("GET", "/Groups", "filter=verified+eq+false&attributes=id%2CdisplayName&sortBy=displayName&sortOrder=descending&startIndex=1&count=100"), 29 | RespondWith(http.StatusOK, groupListResponse, contentTypeJson), 30 | )) 31 | 32 | session := runCommand("list-groups", 33 | "--filter", "verified eq false", 34 | "--attributes", "id,displayName", 35 | "--sortBy", "displayName", 36 | "--sortOrder", "descending", 37 | ) 38 | 39 | Eventually(session).Should(Exit(0)) 40 | }) 41 | 42 | It("understands the --zone flag", func() { 43 | server.RouteToHandler("GET", "/Groups", CombineHandlers( 44 | VerifyRequest("GET", "/Groups", "filter=verified+eq+false&attributes=id%2CdisplayName&sortBy=displayName&sortOrder=descending&startIndex=1&count=100"), 45 | VerifyHeaderKV("X-Identity-Zone-Id", "twilight-zone"), 46 | RespondWith(http.StatusOK, groupListResponse, contentTypeJson), 47 | )) 48 | 49 | session := runCommand("list-groups", 50 | "--filter", "verified eq false", 51 | "--attributes", "id,displayName", 52 | "--sortBy", "displayName", 53 | "--sortOrder", "descending", 54 | "--zone", "twilight-zone", 55 | ) 56 | 57 | Eventually(session).Should(Exit(0)) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /cmd/list_users.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "code.cloudfoundry.org/uaa-cli/help" 7 | "github.com/cloudfoundry-community/go-uaa" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func ListUserValidations(cfg config.Config) error { 12 | if err := cli.EnsureContextInConfig(cfg); err != nil { 13 | return err 14 | } 15 | return nil 16 | } 17 | 18 | func ListUsersCmd(api *uaa.API, printer cli.Printer, filter, sortBy, sortOrder, attributes string) error { 19 | user, err := api.ListAllUsers(filter, sortBy, attributes, uaa.SortOrder(sortOrder)) 20 | if err != nil { 21 | return err 22 | } 23 | return printer.Print(user) 24 | } 25 | 26 | var listUsersCmd = &cobra.Command{ 27 | Use: "list-users", 28 | Aliases: []string{"users", "get-users", "search-users"}, 29 | Short: "Search and list users with SCIM filters", 30 | Long: help.ListUsers(), 31 | PreRun: func(cmd *cobra.Command, args []string) { 32 | cli.NotifyValidationErrors(ListUserValidations(GetSavedConfig()), cmd, log) 33 | }, 34 | Run: func(cmd *cobra.Command, args []string) { 35 | err := ListUsersCmd(GetAPIFromSavedTokenInContext(), cli.NewJsonPrinter(log), filter, sortBy, sortOrder, attributes) 36 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 37 | }, 38 | } 39 | 40 | func init() { 41 | RootCmd.AddCommand(listUsersCmd) 42 | listUsersCmd.Annotations = make(map[string]string) 43 | listUsersCmd.Annotations[USER_CRUD_CATEGORY] = "true" 44 | 45 | listUsersCmd.Flags().StringVarP(&filter, "filter", "", "", `a SCIM filter, or query, e.g. 'userName eq "bob@example.com"'`) 46 | listUsersCmd.Flags().StringVarP(&sortBy, "sortBy", "b", "", `the attribute to sort results by, e.g. "created" or "userName"`) 47 | listUsersCmd.Flags().StringVarP(&sortOrder, "sortOrder", "o", "", `how results should be ordered. One of: [ascending, descending]`) 48 | listUsersCmd.Flags().StringVarP(&attributes, "attributes", "a", "", `include only these comma-separated user attributes to improve query performance`) 49 | listUsersCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in which to list the users") 50 | } 51 | -------------------------------------------------------------------------------- /cmd/list_users_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "code.cloudfoundry.org/uaa-cli/config" 8 | . "code.cloudfoundry.org/uaa-cli/fixtures" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | . "github.com/onsi/gomega/gexec" 12 | . "github.com/onsi/gomega/ghttp" 13 | ) 14 | 15 | var _ = Describe("ListUsers", func() { 16 | 17 | var userListResponse string 18 | 19 | BeforeEach(func() { 20 | cfg := config.NewConfigWithServerURL(server.URL()) 21 | cfg.AddContext(config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 22 | config.WriteConfig(cfg) 23 | userListResponse = fmt.Sprintf(PaginatedResponseTmpl, MarcusUserResponse, DrSeussUserResponse) 24 | }) 25 | 26 | It("executes SCIM queries based on flags", func() { 27 | server.RouteToHandler("GET", "/Users", CombineHandlers( 28 | VerifyRequest("GET", "/Users", "filter=verified+eq+false&attributes=id%2CuserName&sortBy=userName&sortOrder=descending&count=100&startIndex=1"), 29 | RespondWith(http.StatusOK, userListResponse, contentTypeJson), 30 | )) 31 | 32 | session := runCommand( 33 | "list-users", 34 | "--filter", "verified eq false", 35 | "--attributes", "id,userName", 36 | "--sortBy", "userName", 37 | "--sortOrder", "descending", 38 | ) 39 | 40 | Eventually(session).Should(Exit(0)) 41 | }) 42 | 43 | It("understands the --zone flag", func() { 44 | server.RouteToHandler("GET", "/Users", CombineHandlers( 45 | VerifyRequest("GET", "/Users", "filter=verified+eq+false&attributes=id%2CuserName&sortBy=userName&sortOrder=descending&count=100&startIndex=1"), 46 | VerifyHeaderKV("X-Identity-Zone-Id", "foozone"), 47 | RespondWith(http.StatusOK, userListResponse, contentTypeJson), 48 | )) 49 | 50 | session := runCommand( 51 | "list-users", 52 | "--filter", "verified eq false", 53 | "--attributes", "id,userName", 54 | "--sortBy", "userName", 55 | "--sortOrder", "descending", 56 | "--zone", "foozone", 57 | ) 58 | 59 | Eventually(session).Should(Exit(0)) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /cmd/map_group.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/utils" 6 | "github.com/cloudfoundry-community/go-uaa" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func MapGroupCmd(api *uaa.API, printer cli.Printer, externalGroupName, groupName, origin string) error { 11 | if origin == "" { 12 | origin = "ldap" 13 | } 14 | group, err := api.GetGroupByName(groupName, "") 15 | if err != nil { 16 | return err 17 | } 18 | err = api.MapGroup(group.ID, externalGroupName, origin) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | log.Infof("Successfully mapped %v to %v for origin %v", utils.Emphasize(groupName), utils.Emphasize(externalGroupName), utils.Emphasize(origin)) 24 | return nil 25 | } 26 | 27 | var mapGroupCmd = &cobra.Command{ 28 | Use: "map-group EXTERNAL_GROUPNAME GROUPNAME", 29 | Short: "Map uaa groups (scopes) to external groups defined within an external identity provider", 30 | PreRun: func(cmd *cobra.Command, args []string) { 31 | cfg := GetSavedConfig() 32 | cli.NotifyValidationErrors(GroupMappingValidations(cfg, args), cmd, log) 33 | }, 34 | Run: func(cmd *cobra.Command, args []string) { 35 | err := MapGroupCmd(GetAPIFromSavedTokenInContext(), cli.NewJsonPrinter(log), args[0], args[1], origin) 36 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 37 | }, 38 | } 39 | 40 | func init() { 41 | RootCmd.AddCommand(mapGroupCmd) 42 | mapGroupCmd.Annotations = make(map[string]string) 43 | mapGroupCmd.Annotations[GROUP_CRUD_CATEGORY] = "true" 44 | 45 | mapGroupCmd.Flags().StringVarP(&origin, "origin", "", "", "map uaa group to external group for this origin. Defaults to ldap.") 46 | } 47 | -------------------------------------------------------------------------------- /cmd/map_group_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/config" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | . "github.com/onsi/gomega/gbytes" 8 | . "github.com/onsi/gomega/gexec" 9 | ) 10 | 11 | var _ = Describe("MapGroup", func() { 12 | Describe("by default", func() { 13 | BeforeEach(func() { 14 | config.WriteConfig(buildConfig(server.URL())) 15 | }) 16 | 17 | It("Resolves the group name and performs the mapping", func() { 18 | mockGroupLookup("internal-group-id", "internal-group") 19 | mockExternalGroupMapping("external-group", "internal-group-id", "internal-group", "ldap") 20 | 21 | session := runCommand("map-group", "external-group", "internal-group") 22 | //Successfully mapped dan_test_group to external-jeremy-group for origin ldap 23 | Eventually(session).Should(Say(`Successfully mapped internal-group to external-group for origin ldap`)) 24 | Eventually(session).Should(Exit(0)) 25 | Expect(server.ReceivedRequests()).Should(HaveLen(2)) 26 | }) 27 | }) 28 | 29 | Describe("with origin", func() { 30 | BeforeEach(func() { 31 | config.WriteConfig(buildConfig(server.URL())) 32 | }) 33 | 34 | It("Resolves the group name and performs the mapping", func() { 35 | mockGroupLookup("internal-group-id", "internal-group") 36 | mockExternalGroupMapping("external-group", "internal-group-id", "internal-group", "saml") 37 | 38 | session := runCommand("map-group", "external-group", "internal-group", "--origin", "saml") 39 | //Successfully mapped dan_test_group to external-jeremy-group for origin ldap 40 | Eventually(session).Should(Say(`Successfully mapped internal-group to external-group for origin saml`)) 41 | Eventually(session).Should(Exit(0)) 42 | Expect(server.ReceivedRequests()).Should(HaveLen(2)) 43 | }) 44 | }) 45 | 46 | Describe("with invalid input", func() { 47 | BeforeEach(func() { 48 | config.WriteConfig(buildConfig(server.URL())) 49 | }) 50 | 51 | It("fails", func() { 52 | session := runCommand("map-group", "external-group") 53 | Eventually(session.Err).Should(Say(`The positional arguments EXTERNAL_GROUPNAME and GROUPNAME must be specified.`)) 54 | Eventually(session).Should(Exit(1)) 55 | Expect(server.ReceivedRequests()).Should(HaveLen(0)) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /cmd/refresh_token.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "code.cloudfoundry.org/uaa-cli/cli" 8 | "code.cloudfoundry.org/uaa-cli/config" 9 | "code.cloudfoundry.org/uaa-cli/help" 10 | "code.cloudfoundry.org/uaa-cli/utils" 11 | "github.com/cloudfoundry-community/go-uaa" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func RefreshTokenCmd(cfg config.Config, log cli.Logger, tokenFormat string) error { 16 | //TODO: use library function to perform conversion 17 | format := uaa.JSONWebToken 18 | if tokenFormat == "opaque" { 19 | format = uaa.OpaqueToken 20 | } 21 | 22 | api, err := uaa.New( 23 | cfg.GetActiveTarget().BaseUrl, 24 | uaa.WithRefreshToken( 25 | cfg.GetActiveContext().ClientId, 26 | clientSecret, 27 | cfg.GetActiveContext().Token.RefreshToken, 28 | format, 29 | ), 30 | uaa.WithZoneID(cfg.ZoneSubdomain), 31 | uaa.WithSkipSSLValidation(cfg.GetActiveTarget().SkipSSLValidation), 32 | uaa.WithVerbosity(verbose), 33 | ) 34 | log.Infof("Using the refresh_token from the active context to request a new access token for client %v.", utils.Emphasize(cfg.GetActiveContext().ClientId)) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | ctx := cfg.GetActiveContext() 40 | 41 | token, err := api.Token(context.Background()) //TODO: stop making this request for a second time 42 | if err != nil { 43 | return err 44 | } 45 | 46 | ctx.Token = *token 47 | cfg.AddContext(ctx) 48 | config.WriteConfig(cfg) 49 | log.Info("Access token successfully fetched and added to active context.") 50 | return nil 51 | } 52 | 53 | func RefreshTokenValidations(cfg config.Config, clientSecret string) error { 54 | if err := cli.EnsureContextInConfig(cfg); err != nil { 55 | return err 56 | } 57 | if clientSecret == "" { 58 | return cli.MissingArgumentError("client_secret") 59 | } 60 | if cfg.GetActiveContext().ClientId == "" { 61 | return errors.New("A client_id was not found in the active context.") 62 | } 63 | if GetSavedConfig().GetActiveContext().Token.RefreshToken == "" { 64 | return errors.New("A refresh_token was not found in the active context.") 65 | } 66 | 67 | return validateTokenFormatError(tokenFormat) 68 | } 69 | 70 | var refreshTokenCmd = &cobra.Command{ 71 | Use: "refresh-token -s CLIENT_SECRET", 72 | Short: "Obtain an access token using the refresh_token grant type", 73 | Long: help.RefreshToken(), 74 | PreRun: func(cmd *cobra.Command, args []string) { 75 | cfg := GetSavedConfig() 76 | cli.NotifyValidationErrors(RefreshTokenValidations(cfg, clientSecret), cmd, log) 77 | }, 78 | Run: func(cmd *cobra.Command, args []string) { 79 | cfg := GetSavedConfig() 80 | cli.NotifyErrorsWithRetry(RefreshTokenCmd(cfg, log, tokenFormat), log, GetSavedConfig()) 81 | }, 82 | } 83 | 84 | func init() { 85 | RootCmd.AddCommand(refreshTokenCmd) 86 | refreshTokenCmd.Annotations = make(map[string]string) 87 | refreshTokenCmd.Annotations[TOKEN_CATEGORY] = "true" 88 | refreshTokenCmd.Flags().StringVarP(&clientSecret, "client_secret", "s", "", "client secret") 89 | refreshTokenCmd.Flags().StringVarP(&tokenFormat, "format", "", "jwt", "available formats include "+availableFormatsStr()) 90 | } 91 | -------------------------------------------------------------------------------- /cmd/remove_member.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/uaa-cli/cli" 7 | cli_config "code.cloudfoundry.org/uaa-cli/config" 8 | "code.cloudfoundry.org/uaa-cli/utils" 9 | "github.com/cloudfoundry-community/go-uaa" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func RemoveMemberPreRunValidations(config cli_config.Config, args []string) error { 14 | if err := cli.EnsureContextInConfig(config); err != nil { 15 | return err 16 | } 17 | 18 | if len(args) != 2 { 19 | return errors.New("The positional arguments GROUPNAME and USERNAME must be specified.") 20 | } 21 | 22 | return nil 23 | } 24 | 25 | func RemoveMemberCmd(api *uaa.API, groupName, username string, log cli.Logger) error { 26 | group, err := api.GetGroupByName(groupName, "") 27 | if err != nil { 28 | return err 29 | } 30 | 31 | user, err := api.GetUserByUsername(username, "", "") 32 | if err != nil { 33 | return err 34 | } 35 | 36 | err = api.RemoveGroupMember(group.ID, user.ID, "", "") 37 | if err != nil { 38 | return err 39 | } 40 | 41 | log.Infof("User %v successfully removed from group %v", utils.Emphasize(username), utils.Emphasize(groupName)) 42 | 43 | return nil 44 | } 45 | 46 | var removeMemberCmd = &cobra.Command{ 47 | Use: "remove-member GROUPNAME USERNAME", 48 | Short: "Remove a user from a group", 49 | PreRun: func(cmd *cobra.Command, args []string) { 50 | cfg := GetSavedConfig() 51 | cli.NotifyValidationErrors(RemoveMemberPreRunValidations(cfg, args), cmd, log) 52 | }, 53 | Run: func(cmd *cobra.Command, args []string) { 54 | groupName := args[0] 55 | userName := args[1] 56 | cli.NotifyErrorsWithRetry(RemoveMemberCmd(GetAPIFromSavedTokenInContext(), groupName, userName, log), log, GetSavedConfig()) 57 | }, 58 | } 59 | 60 | func init() { 61 | RootCmd.AddCommand(removeMemberCmd) 62 | removeMemberCmd.Annotations = make(map[string]string) 63 | removeMemberCmd.Annotations[GROUP_CRUD_CATEGORY] = "true" 64 | } 65 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "code.cloudfoundry.org/uaa-cli/cli" 8 | "code.cloudfoundry.org/uaa-cli/config" 9 | "code.cloudfoundry.org/uaa-cli/help" 10 | "code.cloudfoundry.org/uaa-cli/version" 11 | "github.com/cloudfoundry-community/go-uaa" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var cfgFile config.Config 16 | var log cli.Logger 17 | 18 | // Token flags 19 | var ( 20 | password string 21 | username string 22 | tokenFormat string 23 | ) 24 | 25 | // Global flags 26 | var ( 27 | skipSSLValidation bool 28 | verbose bool 29 | ) 30 | 31 | // Client flags 32 | var ( 33 | clientSecret string 34 | authorizedGrantTypes string 35 | authorities string 36 | accessTokenValidity int64 37 | refreshTokenValidity int64 38 | displayName string 39 | scope string 40 | redirectUri string 41 | clone string 42 | zoneSubdomain string 43 | port int 44 | ) 45 | 46 | // Users flags 47 | var ( 48 | familyName string 49 | givenName string 50 | emails []string 51 | phoneNumbers []string 52 | origin string 53 | userPassword string 54 | ) 55 | 56 | // Group flags 57 | var ( 58 | groupDescription string 59 | ) 60 | 61 | // SCIM filters 62 | var ( 63 | filter string 64 | sortBy string 65 | sortOrder string 66 | attributes string 67 | count int 68 | startIndex int 69 | ) 70 | 71 | // Curl flags 72 | var ( 73 | method string 74 | data string 75 | headers []string 76 | ) 77 | 78 | // Command categories 79 | const ( 80 | INTRO_CATEGORY = "Getting Started" 81 | TOKEN_CATEGORY = "Getting Tokens" 82 | CLIENT_CRUD_CATEGORY = "Managing Clients" 83 | USER_CRUD_CATEGORY = "Managing Users" 84 | GROUP_CRUD_CATEGORY = "Managing Groups" 85 | MISC_CATEGORY = "Miscellaneous" 86 | ) 87 | 88 | var RootCmd = cobra.Command{ 89 | Use: "uaa", 90 | Short: "A cli for interacting with UAAs", 91 | Long: help.Root(version.VersionString()), 92 | } 93 | 94 | // Execute adds all child commands to the root command and sets flags appropriately. 95 | // This is called by main.main(). It only needs to happen once to the rootCmd. 96 | func Execute() { 97 | if err := RootCmd.Execute(); err != nil { 98 | fmt.Println(err) 99 | os.Exit(1) 100 | } 101 | } 102 | 103 | func init() { 104 | cobra.OnInitialize(initConfig) 105 | RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "See additional info on HTTP requests") 106 | RootCmd.Annotations = make(map[string]string) 107 | RootCmd.Annotations[INTRO_CATEGORY] = "true" 108 | RootCmd.Annotations[TOKEN_CATEGORY] = "true" 109 | RootCmd.Annotations[CLIENT_CRUD_CATEGORY] = "true" 110 | RootCmd.Annotations[USER_CRUD_CATEGORY] = "true" 111 | RootCmd.Annotations[GROUP_CRUD_CATEGORY] = "true" 112 | RootCmd.Annotations[MISC_CATEGORY] = "true" 113 | RootCmd.SetUsageTemplate(`Usage:{{if .Runnable}} 114 | {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} 115 | {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} 116 | 117 | Aliases: 118 | {{.NameAndAliases}}{{end}}{{if .HasExample}} 119 | 120 | Examples: 121 | {{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{ $cmds := .Commands }}{{ range $category, $value := .Annotations }} 122 | 123 | {{ $category }}: {{range $cmd := $cmds}}{{if (or (and $cmd.IsAvailableCommand (eq (index $cmd.Annotations $category) "true")) (and (eq $cmd.Name "help") (eq $category "Getting Started")))}} 124 | {{rpad $cmd.Name $cmd.NamePadding }} {{$cmd.Short}}{{end}}{{end}}{{ end }}{{ end }}{{if .HasAvailableLocalFlags}} 125 | 126 | Flags: 127 | {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} 128 | 129 | Global Flags: 130 | {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} 131 | 132 | Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} 133 | {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} 134 | 135 | Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} 136 | `) 137 | 138 | log = cli.NewLogger(os.Stdout, os.Stdout, os.Stdout, os.Stderr) 139 | } 140 | 141 | func initConfig() { 142 | // Startup tasks 143 | } 144 | 145 | func GetLogger() *cli.Logger { 146 | return &log 147 | } 148 | 149 | func GetSavedConfig() config.Config { 150 | cfgFile = config.ReadConfig() 151 | cfgFile.Verbose = verbose 152 | cfgFile.ZoneSubdomain = zoneSubdomain 153 | return cfgFile 154 | } 155 | 156 | func NewApiFromSavedConfig() *uaa.API { 157 | cfg := GetSavedConfig() 158 | token := cfg.GetActiveContext().Token 159 | api, err := uaa.New( 160 | cfg.GetActiveTarget().BaseUrl, 161 | uaa.WithToken(&token), 162 | uaa.WithZoneID(cfg.ZoneSubdomain), 163 | uaa.WithSkipSSLValidation(cfg.GetActiveTarget().SkipSSLValidation), 164 | uaa.WithVerbosity(verbose), 165 | ) 166 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 167 | return api 168 | } 169 | -------------------------------------------------------------------------------- /cmd/set_client_secret.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/uaa-cli/cli" 7 | "code.cloudfoundry.org/uaa-cli/config" 8 | "code.cloudfoundry.org/uaa-cli/utils" 9 | "github.com/cloudfoundry-community/go-uaa" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func SetClientSecretValidation(cfg config.Config, args []string, clientSecret string) error { 14 | if err := cli.EnsureContextInConfig(cfg); err != nil { 15 | return err 16 | } 17 | if len(args) == 0 { 18 | return cli.MissingArgumentError("client_id") 19 | } 20 | if clientSecret == "" { 21 | return cli.MissingArgumentError("client_secret") 22 | } 23 | return nil 24 | } 25 | 26 | func SetClientSecretCmd(api *uaa.API, log cli.Logger, clientId, clientSecret string) error { 27 | err := api.ChangeClientSecret(clientId, clientSecret) 28 | if err != nil { 29 | return errors.New("The secret for client " + clientId + " was not updated.") 30 | } 31 | log.Infof("The secret for client %v has been successfully updated.", utils.Emphasize(clientId)) 32 | return nil 33 | } 34 | 35 | var setClientSecretCmd = &cobra.Command{ 36 | Use: "set-client-secret CLIENT_ID -s CLIENT_SECRET", 37 | Short: "Update secret for a client", 38 | PreRun: func(cmd *cobra.Command, args []string) { 39 | cli.NotifyValidationErrors(SetClientSecretValidation(GetSavedConfig(), args, clientSecret), cmd, log) 40 | }, 41 | Run: func(cmd *cobra.Command, args []string) { 42 | cli.NotifyErrorsWithRetry(SetClientSecretCmd(GetAPIFromSavedTokenInContext(), log, args[0], clientSecret), log, GetSavedConfig()) 43 | }, 44 | } 45 | 46 | func init() { 47 | RootCmd.AddCommand(setClientSecretCmd) 48 | setClientSecretCmd.Annotations = make(map[string]string) 49 | setClientSecretCmd.Annotations[CLIENT_CRUD_CATEGORY] = "true" 50 | setClientSecretCmd.Flags().StringVarP(&clientSecret, "client_secret", "s", "", "new client secret") 51 | setClientSecretCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain where the client resides") 52 | } 53 | -------------------------------------------------------------------------------- /cmd/set_client_secret_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "code.cloudfoundry.org/uaa-cli/config" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | . "github.com/onsi/gomega/gbytes" 10 | . "github.com/onsi/gomega/gexec" 11 | . "github.com/onsi/gomega/ghttp" 12 | ) 13 | 14 | var _ = Describe("SetClientSecret", func() { 15 | BeforeEach(func() { 16 | c := config.NewConfigWithServerURL(server.URL()) 17 | c.AddContext(config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 18 | config.WriteConfig(c) 19 | }) 20 | 21 | It("can update client secrets", func() { 22 | server.RouteToHandler("PUT", "/oauth/clients/shinyclient/secret", CombineHandlers( 23 | VerifyRequest("PUT", "/oauth/clients/shinyclient/secret"), 24 | VerifyJSON(`{"clientId":"shinyclient","secret":"shinysecret"}`), 25 | VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"), 26 | RespondWith(http.StatusOK, "{}"), 27 | ), 28 | ) 29 | 30 | session := runCommand("set-client-secret", "shinyclient", "-s", "shinysecret") 31 | 32 | Expect(session.Out).To(Say("The secret for client shinyclient has been successfully updated.")) 33 | Eventually(session).Should(Exit(0)) 34 | }) 35 | 36 | It("displays error when request fails", func() { 37 | server.RouteToHandler("PUT", "/oauth/clients/shinyclient/secret", CombineHandlers( 38 | VerifyRequest("PUT", "/oauth/clients/shinyclient/secret"), 39 | VerifyJSON(`{"clientId":"shinyclient","secret":"shinysecret"}`), 40 | VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"), 41 | RespondWith(http.StatusUnauthorized, "{}"), 42 | ), 43 | ) 44 | 45 | session := runCommand("set-client-secret", "shinyclient", "-s", "shinysecret") 46 | 47 | Expect(session.Err).To(Say("The secret for client shinyclient was not updated.")) 48 | Expect(session.Out).To(Say("Retry with --verbose for more information.")) 49 | Eventually(session).Should(Exit(1)) 50 | }) 51 | 52 | It("complains when there is no active target", func() { 53 | config.WriteConfig(config.NewConfig()) 54 | session := runCommand("set-client-secret", "shinyclient", "-s", "shinysecret") 55 | 56 | Expect(session.Err).To(Say("You must set a target in order to use this command.")) 57 | Eventually(session).Should(Exit(1)) 58 | }) 59 | 60 | It("complains when there is no active context", func() { 61 | c := config.NewConfig() 62 | t := config.NewTarget() 63 | c.AddTarget(t) 64 | config.WriteConfig(c) 65 | session := runCommand("set-client-secret", "shinyclient", "-s", "shinysecret") 66 | 67 | Expect(session.Err).To(Say("You must have a token in your context to perform this command.")) 68 | Eventually(session).Should(Exit(1)) 69 | }) 70 | 71 | It("supports zone switching", func() { 72 | server.RouteToHandler("PUT", "/oauth/clients/shinyclient/secret", CombineHandlers( 73 | VerifyRequest("PUT", "/oauth/clients/shinyclient/secret"), 74 | VerifyJSON(`{"clientId":"shinyclient","secret":"shinysecret"}`), 75 | VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"), 76 | VerifyHeaderKV("X-Identity-Zone-Id", "twilight-zone"), 77 | RespondWith(http.StatusOK, "{}"), 78 | ), 79 | ) 80 | 81 | session := runCommand("set-client-secret", "shinyclient", "-s", "shinysecret", "--zone", "twilight-zone") 82 | 83 | Expect(session.Out).To(Say("The secret for client shinyclient has been successfully updated.")) 84 | Eventually(session).Should(Exit(0)) 85 | }) 86 | 87 | It("complains when no secret is provided", func() { 88 | session := runCommand("set-client-secret", "shinyclient") 89 | 90 | Expect(session.Err).To(Say("Missing argument `client_secret` must be specified.")) 91 | Eventually(session).Should(Exit(1)) 92 | }) 93 | 94 | It("complains when no clientid is provided", func() { 95 | session := runCommand("set-client-secret", "-s", "shinysecret") 96 | 97 | Expect(session.Err).To(Say("Missing argument `client_id` must be specified.")) 98 | Eventually(session).Should(Exit(1)) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /cmd/target.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "code.cloudfoundry.org/uaa-cli/cli" 8 | "code.cloudfoundry.org/uaa-cli/config" 9 | "code.cloudfoundry.org/uaa-cli/utils" 10 | "github.com/cloudfoundry-community/go-uaa" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | type TargetStatus struct { 15 | Target string 16 | Status string 17 | UaaVersion string 18 | SkipSSLValidation bool 19 | } 20 | 21 | func printTarget(log cli.Logger, target config.Target, status string, version string) error { 22 | return cli.NewJsonPrinter(log).Print(TargetStatus{target.BaseUrl, status, version, target.SkipSSLValidation}) 23 | } 24 | 25 | func ShowTargetCmd(api *uaa.API, cfg config.Config, log cli.Logger) error { 26 | target := cfg.GetActiveTarget() 27 | 28 | if target.BaseUrl == "" { 29 | return printTarget(log, target, "", "") 30 | } 31 | 32 | info, err := api.GetInfo() 33 | if err != nil { 34 | _ = printTarget(log, target, "ERROR", "unknown") 35 | return errors.New("There was an error while fetching status info about the current target.") 36 | } 37 | 38 | return printTarget(log, target, "OK", info.App.Version) 39 | } 40 | 41 | func UpdateTargetCmd(cfg config.Config, newTarget string, log cli.Logger) error { 42 | target := config.Target{ 43 | SkipSSLValidation: skipSSLValidation, 44 | BaseUrl: newTarget, 45 | } 46 | cfg.AddTarget(target) 47 | 48 | api := GetUnauthenticatedAPIFromConfig(cfg) 49 | _, err := api.GetInfo() 50 | 51 | if err != nil { 52 | return errors.New(fmt.Sprintf("The target %s could not be set: %v", newTarget, err.Error())) 53 | } 54 | 55 | config.WriteConfig(cfg) 56 | log.Info("Target set to " + utils.Emphasize(newTarget)) 57 | return nil 58 | } 59 | 60 | var targetCmd = &cobra.Command{ 61 | Use: "target UAA_URL", 62 | Aliases: []string{"api"}, 63 | Short: "Set the url of the UAA you'd like to target", 64 | Run: func(cmd *cobra.Command, args []string) { 65 | cfg := GetSavedConfig() 66 | target := cfg.GetActiveTarget() 67 | 68 | if len(args) == 0 { 69 | var api *uaa.API 70 | if target.BaseUrl != "" { 71 | api = GetUnauthenticatedAPI() 72 | } 73 | cli.NotifyErrorsWithRetry(ShowTargetCmd(api, cfg, log), log, GetSavedConfig()) 74 | } else { 75 | cli.NotifyErrorsWithRetry(UpdateTargetCmd(cfg, args[0], log), log, GetSavedConfig()) 76 | } 77 | }, 78 | } 79 | 80 | func init() { 81 | RootCmd.AddCommand(targetCmd) 82 | targetCmd.Flags().BoolVarP(&skipSSLValidation, "skip-ssl-validation", "k", false, "Disable security validation on requests to this target") 83 | targetCmd.Annotations = make(map[string]string) 84 | targetCmd.Annotations[INTRO_CATEGORY] = "true" 85 | } 86 | -------------------------------------------------------------------------------- /cmd/target_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "code.cloudfoundry.org/uaa-cli/config" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | . "github.com/onsi/gomega/gbytes" 10 | . "github.com/onsi/gomega/gexec" 11 | . "github.com/onsi/gomega/ghttp" 12 | ) 13 | 14 | const InfoResponseJson = `{ 15 | "app": { 16 | "version": "4.5.0" 17 | }, 18 | "links": { 19 | "uaa": "https://uaa.run.pivotal.io", 20 | "passwd": "https://account.run.pivotal.io/forgot-password", 21 | "login": "https://login.run.pivotal.io", 22 | "register": "https://account.run.pivotal.io/sign-up" 23 | }, 24 | "zone_name": "uaa", 25 | "entityID": "login.run.pivotal.io", 26 | "commit_id": "df80f63", 27 | "idpDefinitions": {}, 28 | "prompts": { 29 | "username": [ 30 | "text", 31 | "Email" 32 | ], 33 | "password": [ 34 | "password", 35 | "Password" 36 | ] 37 | }, 38 | "timestamp": "2017-07-21T22:45:01+0000" 39 | }` 40 | 41 | var _ = Describe("Target", func() { 42 | Describe("when no new url is provided", func() { 43 | Describe("and a target was previously set", func() { 44 | BeforeEach(func() { 45 | c := config.NewConfigWithServerURL(server.URL()) 46 | config.WriteConfig(c) 47 | }) 48 | 49 | It("shows the currently set target and UAA version", func() { 50 | server.RouteToHandler("GET", "/info", 51 | RespondWith(http.StatusOK, InfoResponseJson, contentTypeJson), 52 | ) 53 | 54 | session := runCommand("target") 55 | 56 | Eventually(session).Should(Exit(0)) 57 | expectedJson := `{ "Target": "` + server.URL() + `", "Status": "OK", "UaaVersion": "4.5.0", "SkipSSLValidation": false }` 58 | Eventually(session.Out.Contents()).Should(MatchJSON(expectedJson)) 59 | }) 60 | 61 | It("shows when UAA can't be reached", func() { 62 | server.RouteToHandler("GET", "/info", 63 | RespondWith(http.StatusBadRequest, ""), 64 | ) 65 | 66 | session := runCommand("target") 67 | 68 | Eventually(session.Out).Should(Say(`"Status": "ERROR"`)) 69 | Eventually(session.Out).Should(Say(`"UaaVersion": "unknown"`)) 70 | Eventually(session).Should(Exit(1)) 71 | }) 72 | }) 73 | 74 | Describe("and a target was never set", func() { 75 | It("displays empty target", func() { 76 | session := runCommand("target") 77 | 78 | Eventually(session).Should(Exit(0)) 79 | expectedJson := `{ "Target": "", "Status": "", "UaaVersion": "", "SkipSSLValidation": false }` 80 | Eventually(session.Out.Contents()).Should(MatchJSON(expectedJson)) 81 | }) 82 | }) 83 | }) 84 | 85 | Describe("when a new url is provided", func() { 86 | Describe("when the url is a valid UAA", func() { 87 | BeforeEach(func() { 88 | server.RouteToHandler("GET", "/info", 89 | RespondWith(http.StatusOK, InfoResponseJson, contentTypeJson), 90 | ) 91 | 92 | c := config.NewConfig() 93 | config.WriteConfig(c) 94 | }) 95 | 96 | It("updates the saved context", func() { 97 | Expect(config.ReadConfig().GetActiveTarget().BaseUrl).To(Equal("")) 98 | 99 | runCommand("target", server.URL()) 100 | 101 | Expect(config.ReadConfig().GetActiveTarget().BaseUrl).NotTo(Equal("")) 102 | Expect(config.ReadConfig().GetActiveTarget().BaseUrl).To(Equal(server.URL())) 103 | }) 104 | 105 | It("displays a success message", func() { 106 | session := runCommand("target", server.URL()) 107 | 108 | Eventually(session).Should(Exit(0)) 109 | Eventually(session.Out).Should(Say("Target set to " + server.URL())) 110 | }) 111 | 112 | It("respects the --skip-ssl-validation flag", func() { 113 | runCommand("target", server.URL()) 114 | Expect(config.ReadConfig().GetActiveTarget().SkipSSLValidation).To(BeFalse()) 115 | 116 | runCommand("target", server.URL(), "--skip-ssl-validation") 117 | Expect(config.ReadConfig().GetActiveTarget().SkipSSLValidation).To(BeTrue()) 118 | }) 119 | }) 120 | 121 | Describe("when the UAA cannot be reached", func() { 122 | BeforeEach(func() { 123 | server.RouteToHandler("GET", "/info", 124 | RespondWith(http.StatusNotFound, ""), 125 | ) 126 | 127 | c := config.NewConfigWithServerURL("http://someuaa.com") 128 | config.WriteConfig(c) 129 | }) 130 | 131 | It("does not update the saved context", func() { 132 | runCommand("target", server.URL()) 133 | 134 | Expect(config.ReadConfig().GetActiveTarget().BaseUrl).To(Equal("http://someuaa.com")) 135 | }) 136 | 137 | It("displays an error message", func() { 138 | session := runCommand("target", server.URL()) 139 | 140 | Eventually(session).Should(Exit(1)) 141 | Eventually(session.Err).Should(Say("The target " + server.URL() + " could not be set.")) 142 | }) 143 | }) 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /cmd/unmap_group.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/utils" 6 | "github.com/cloudfoundry-community/go-uaa" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func UnmapGroupCmd(api *uaa.API, printer cli.Printer, externalGroupName, groupName, origin string) error { 11 | if origin == "" { 12 | origin = "ldap" 13 | } 14 | 15 | group, err := api.GetGroupByName(groupName, "") 16 | if err != nil { 17 | return err 18 | } 19 | err = api.UnmapGroup(group.ID, externalGroupName, origin) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | log.Infof("Successfully unmapped %v from %v for origin %v", utils.Emphasize(groupName), utils.Emphasize(externalGroupName), utils.Emphasize(origin)) 25 | return nil 26 | } 27 | 28 | var unmapGroupCmd = &cobra.Command{ 29 | Use: "unmap-group EXTERNAL_GROUPNAME GROUPNAME", 30 | Short: "Unmaps an external group defined within an external identity provider from a uaa group (scope)", 31 | PreRun: func(cmd *cobra.Command, args []string) { 32 | cfg := GetSavedConfig() 33 | cli.NotifyValidationErrors(GroupMappingValidations(cfg, args), cmd, log) 34 | }, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | err := UnmapGroupCmd(GetAPIFromSavedTokenInContext(), cli.NewJsonPrinter(log), args[0], args[1], origin) 37 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 38 | }, 39 | } 40 | 41 | func init() { 42 | RootCmd.AddCommand(unmapGroupCmd) 43 | unmapGroupCmd.Annotations = make(map[string]string) 44 | unmapGroupCmd.Annotations[GROUP_CRUD_CATEGORY] = "true" 45 | 46 | unmapGroupCmd.Flags().StringVarP(&origin, "origin", "", "", "map uaa group to external group for this origin. Defaults to ldap.") 47 | } 48 | -------------------------------------------------------------------------------- /cmd/unmap_group_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/config" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | . "github.com/onsi/gomega/gbytes" 8 | . "github.com/onsi/gomega/gexec" 9 | ) 10 | 11 | var _ = Describe("UnmapGroup", func() { 12 | Describe("by default", func() { 13 | BeforeEach(func() { 14 | config.WriteConfig(buildConfig(server.URL())) 15 | }) 16 | 17 | It("Resolves the group name and performs the unmapping", func() { 18 | mockGroupLookup("internal-group-id", "internal-group") 19 | mockExternalGroupUnmapping("external-group", "internal-group-id", "internal-group", "ldap") 20 | 21 | session := runCommand("unmap-group", "external-group", "internal-group") 22 | Eventually(session).Should(Say(`Successfully unmapped internal-group from external-group for origin ldap`)) 23 | Eventually(session).Should(Exit(0)) 24 | Expect(server.ReceivedRequests()).Should(HaveLen(2)) 25 | }) 26 | }) 27 | 28 | Describe("with origin", func() { 29 | BeforeEach(func() { 30 | config.WriteConfig(buildConfig(server.URL())) 31 | }) 32 | 33 | It("Resolves the group name and performs the unmapping", func() { 34 | mockGroupLookup("internal-group-id", "internal-group") 35 | mockExternalGroupUnmapping("external-group", "internal-group-id", "internal-group", "saml") 36 | 37 | session := runCommand("unmap-group", "external-group", "internal-group", "--origin", "saml") 38 | Eventually(session).Should(Say(`Successfully unmapped internal-group from external-group for origin saml`)) 39 | Eventually(session).Should(Exit(0)) 40 | Expect(server.ReceivedRequests()).Should(HaveLen(2)) 41 | }) 42 | }) 43 | 44 | Describe("with invalid input", func() { 45 | BeforeEach(func() { 46 | config.WriteConfig(buildConfig(server.URL())) 47 | }) 48 | 49 | It("fails", func() { 50 | session := runCommand("unmap-group", "external-group") 51 | Eventually(session.Err).Should(Say(`The positional arguments EXTERNAL_GROUPNAME and GROUPNAME must be specified.`)) 52 | Eventually(session).Should(Exit(1)) 53 | Expect(server.ReceivedRequests()).Should(HaveLen(0)) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /cmd/update_client.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/uaa-cli/cli" 7 | "code.cloudfoundry.org/uaa-cli/config" 8 | "code.cloudfoundry.org/uaa-cli/utils" 9 | "github.com/cloudfoundry-community/go-uaa" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func UpdateClientValidations(cfg config.Config, args []string, clientSecret string) error { 14 | if err := cli.EnsureContextInConfig(cfg); err != nil { 15 | return err 16 | } 17 | if len(args) < 1 { 18 | return cli.MissingArgumentError("client_id") 19 | } 20 | if clientSecret != "" { 21 | return errors.New(`Client not updated. Please see "uaa set-client-secret -h" to learn more about changing client secrets.`) 22 | } 23 | return nil 24 | } 25 | 26 | func UpdateClientCmd(api *uaa.API, clientId, displayName, authorizedGrantTypes, authorities, redirectUri, scope string, accessTokenValidity, refreshTokenValidity int64) error { 27 | toUpdate := uaa.Client{ 28 | ClientID: clientId, 29 | DisplayName: displayName, 30 | AuthorizedGrantTypes: arrayify(authorizedGrantTypes), 31 | Authorities: arrayify(authorities), 32 | RedirectURI: arrayify(redirectUri), 33 | Scope: arrayify(scope), 34 | AccessTokenValidity: accessTokenValidity, 35 | RefreshTokenValidity: refreshTokenValidity, 36 | } 37 | 38 | updated, err := api.UpdateClient(toUpdate) 39 | if err != nil { 40 | return errors.New("An error occurred while updating the client.") 41 | } 42 | 43 | log.Infof("The client %v has been successfully updated.", utils.Emphasize(clientId)) 44 | return cli.NewJsonPrinter(log).Print(updated) 45 | 46 | } 47 | 48 | var updateClientCmd = &cobra.Command{ 49 | Use: "update-client CLIENT_ID", 50 | Short: "Update an OAuth client registration in the UAA", 51 | PreRun: func(cmd *cobra.Command, args []string) { 52 | cli.NotifyValidationErrors(UpdateClientValidations(GetSavedConfig(), args, clientSecret), cmd, log) 53 | }, 54 | Run: func(cmd *cobra.Command, args []string) { 55 | cli.NotifyErrorsWithRetry(UpdateClientCmd(GetAPIFromSavedTokenInContext(), args[0], displayName, authorizedGrantTypes, authorities, redirectUri, scope, accessTokenValidity, refreshTokenValidity), log, GetSavedConfig()) 56 | }, 57 | } 58 | 59 | func init() { 60 | RootCmd.AddCommand(updateClientCmd) 61 | updateClientCmd.Annotations = make(map[string]string) 62 | updateClientCmd.Annotations[CLIENT_CRUD_CATEGORY] = "true" 63 | updateClientCmd.Flags().StringVarP(&clientSecret, "client_secret", "s", "", "client secret") 64 | updateClientCmd.Flag("client_secret").Hidden = true 65 | 66 | updateClientCmd.Flags().StringVarP(&authorizedGrantTypes, "authorized_grant_types", "", "", "list of grant types allowed with this client.") 67 | updateClientCmd.Flags().StringVarP(&authorities, "authorities", "", "", "scopes requested by client during client_credentials grant") 68 | updateClientCmd.Flags().StringVarP(&scope, "scope", "", "", "scopes requested by client during authorization_code, implicit, or password grants") 69 | updateClientCmd.Flags().Int64VarP(&accessTokenValidity, "access_token_validity", "", 0, "the time in seconds before issued access tokens expire") 70 | updateClientCmd.Flags().Int64VarP(&refreshTokenValidity, "refresh_token_validity", "", 0, "the time in seconds before issued refrsh tokens expire") 71 | updateClientCmd.Flags().StringVarP(&displayName, "display_name", "", "", "a friendly human-readable name for this client") 72 | updateClientCmd.Flags().StringVarP(&redirectUri, "redirect_uri", "", "", "callback urls allowed for use in authorization_code and implicit grants") 73 | updateClientCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in which to update the client") 74 | } 75 | -------------------------------------------------------------------------------- /cmd/userinfo.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cli" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | "code.cloudfoundry.org/uaa-cli/help" 7 | "github.com/cloudfoundry-community/go-uaa" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func UserinfoValidations(cfg config.Config) error { 12 | return cli.EnsureContextInConfig(cfg) 13 | } 14 | 15 | func UserinfoCmd(api *uaa.API) error { 16 | i, err := api.GetMe() 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return cli.NewJsonPrinter(log).Print(i) 22 | } 23 | 24 | var userinfoCmd = cobra.Command{ 25 | Use: "userinfo", 26 | Short: "See claims about the authenticated user", 27 | Aliases: []string{"me"}, 28 | Long: help.Userinfo(), 29 | PreRun: func(cmd *cobra.Command, args []string) { 30 | cli.NotifyValidationErrors(UserinfoValidations(GetSavedConfig()), cmd, log) 31 | }, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | err := UserinfoCmd(GetAPIFromSavedTokenInContext()) 34 | cli.NotifyErrorsWithRetry(err, log, GetSavedConfig()) 35 | }, 36 | } 37 | 38 | func init() { 39 | RootCmd.AddCommand(&userinfoCmd) 40 | userinfoCmd.Annotations = make(map[string]string) 41 | userinfoCmd.Annotations[MISC_CATEGORY] = "true" 42 | } 43 | -------------------------------------------------------------------------------- /cmd/userinfo_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/cmd" 5 | "code.cloudfoundry.org/uaa-cli/config" 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | . "github.com/onsi/gomega/gbytes" 9 | . "github.com/onsi/gomega/gexec" 10 | . "github.com/onsi/gomega/ghttp" 11 | "net/http" 12 | ) 13 | 14 | var _ = Describe("Userinfo", func() { 15 | Describe("and a target was previously set", func() { 16 | userinfoJson := `{ 17 | "user_id": "d6ef6c2e-02f6-477a-a7c6-18e27f9a6e87", 18 | "sub": "d6ef6c2e-02f6-477a-a7c6-18e27f9a6e87", 19 | "user_name": "charlieb", 20 | "given_name": "Charlie", 21 | "family_name": "Brown", 22 | "email": "charlieb@peanuts.com", 23 | "phone_number": "", 24 | "previous_logon_time": 1503123277743, 25 | "name": "Charlie Brown" 26 | }` 27 | 28 | BeforeEach(func() { 29 | c := config.NewConfigWithServerURL(server.URL()) 30 | c.AddContext(config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")) 31 | config.WriteConfig(c) 32 | }) 33 | 34 | It("shows the info response", func() { 35 | server.RouteToHandler("GET", "/userinfo", CombineHandlers( 36 | RespondWith(200, userinfoJson), 37 | VerifyRequest("GET", "/userinfo", "scheme=openid"), 38 | VerifyHeaderKV("Accept", "application/json"), 39 | VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"), 40 | )) 41 | 42 | session := runCommand("userinfo") 43 | 44 | Eventually(session).Should(Exit(0)) 45 | outputBytes := session.Out.Contents() 46 | Expect(outputBytes).To(MatchJSON(userinfoJson)) 47 | }) 48 | 49 | It("handles request errors", func() { 50 | server.RouteToHandler("GET", "/userinfo", 51 | RespondWith(http.StatusBadRequest, ""), 52 | ) 53 | 54 | session := runCommand("userinfo") 55 | 56 | Eventually(session).Should(Exit(1)) 57 | Expect(session.Err).To(Say("An error occurred while calling " + server.URL() + "/userinfo")) 58 | }) 59 | }) 60 | 61 | Describe("Validations", func() { 62 | It("requires a target", func() { 63 | err := cmd.UserinfoValidations(config.Config{}) 64 | Expect(err).To(HaveOccurred()) 65 | Expect(err.Error()).To(Equal("You must set a target in order to use this command.")) 66 | }) 67 | 68 | It("requires a context", func() { 69 | cfg := config.NewConfigWithServerURL("http://localhost:9090") 70 | 71 | err := cmd.UserinfoValidations(cfg) 72 | Expect(err).To(HaveOccurred()) 73 | Expect(err.Error()).To(Equal("You must have a token in your context to perform this command.")) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /cmd/validations.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/utils" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | func avalableFormats() []string { 10 | return []string{"jwt", "opaque"} 11 | } 12 | 13 | func availableFormatsStr() string { 14 | return utils.StringSliceStringifier(avalableFormats()) 15 | } 16 | 17 | func validateTokenFormatError(tokenFormat string) error { 18 | if !utils.Contains(avalableFormats(), tokenFormat) { 19 | return errors.New(fmt.Sprintf(`The token format "%v" is unknown. Available formats: %v`, tokenFormat, availableFormatsStr())) 20 | } 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/version" 5 | "fmt" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var versionCmd = &cobra.Command{ 10 | Use: "version", 11 | Short: "Print CLI version", 12 | Run: func(cmd *cobra.Command, args []string) { 13 | fmt.Println(version.VersionString()) 14 | }, 15 | } 16 | 17 | func init() { 18 | RootCmd.AddCommand(versionCmd) 19 | versionCmd.Annotations = make(map[string]string) 20 | versionCmd.Annotations[INTRO_CATEGORY] = "true" 21 | } 22 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "golang.org/x/oauth2" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "time" 11 | ) 12 | 13 | const ( 14 | REFRESH_TOKEN = GrantType("refresh_token") 15 | AUTHCODE = GrantType("authorization_code") 16 | IMPLICIT = GrantType("implicit") 17 | PASSWORD = GrantType("password") 18 | CLIENT_CREDENTIALS = GrantType("client_credentials") 19 | ) 20 | 21 | type Config struct { 22 | Verbose bool 23 | ZoneSubdomain string 24 | Targets map[string]Target 25 | ActiveTargetName string 26 | } 27 | 28 | type Target struct { 29 | BaseUrl string 30 | SkipSSLValidation bool 31 | Contexts map[string]UaaContext 32 | ActiveContextName string 33 | } 34 | 35 | type GrantType string 36 | 37 | type UaaContext struct { 38 | ClientId string `json:"client_id"` 39 | GrantType GrantType `json:"grant_type"` 40 | Username string `json:"username"` 41 | Token oauth2.Token 42 | } 43 | 44 | func NewConfig() Config { 45 | c := Config{} 46 | c.Targets = map[string]Target{} 47 | return c 48 | } 49 | 50 | func NewConfigWithServerURL(url string) Config { 51 | c := NewConfig() 52 | t := NewTarget() 53 | t.BaseUrl = url 54 | c.AddTarget(t) 55 | return c 56 | } 57 | 58 | func NewContextWithToken(accessToken string) UaaContext { 59 | ctx := UaaContext{ 60 | Token: oauth2.Token{ 61 | AccessToken: accessToken, 62 | Expiry: time.Now().Add(1 * time.Hour), 63 | }, 64 | } 65 | return ctx 66 | } 67 | 68 | func NewTarget() Target { 69 | t := Target{} 70 | t.Contexts = map[string]UaaContext{} 71 | return t 72 | } 73 | 74 | func (c Config) GetActiveTarget() Target { 75 | return c.Targets[c.ActiveTargetName] 76 | } 77 | 78 | func (c *Config) AddTarget(newTarget Target) { 79 | c.Targets[newTarget.name()] = newTarget 80 | c.ActiveTargetName = newTarget.name() 81 | } 82 | 83 | func (c *Config) AddContext(newContext UaaContext) { 84 | if c.Targets == nil { 85 | c.Targets = map[string]Target{} 86 | } 87 | t := c.Targets[c.ActiveTargetName] 88 | if t.Contexts == nil { 89 | t.Contexts = map[string]UaaContext{} 90 | } 91 | t.Contexts[newContext.name()] = newContext 92 | t.ActiveContextName = newContext.name() 93 | c.AddTarget(t) 94 | } 95 | 96 | func (c Config) GetActiveContext() UaaContext { 97 | return c.GetActiveTarget().GetActiveContext() 98 | } 99 | 100 | func (t Target) GetActiveContext() UaaContext { 101 | return t.Contexts[t.ActiveContextName] 102 | } 103 | 104 | func (t Target) name() string { 105 | return "url:" + t.BaseUrl 106 | } 107 | 108 | func (uc UaaContext) name() string { 109 | return fmt.Sprintf("client:%v user:%v grant_type:%v", uc.ClientId, uc.Username, uc.GrantType) 110 | } 111 | 112 | func ConfigDir() string { 113 | return path.Join(userHomeDir(), ".uaa") 114 | } 115 | 116 | func ConfigPath() string { 117 | return path.Join(ConfigDir(), "config.json") 118 | } 119 | 120 | func ReadConfig() Config { 121 | c := NewConfig() 122 | 123 | data, err := ioutil.ReadFile(ConfigPath()) 124 | if err != nil { 125 | return c 126 | } 127 | 128 | json.Unmarshal(data, &c) 129 | 130 | return c 131 | } 132 | 133 | func WriteConfig(c Config) error { 134 | err := makeDirectory() 135 | if err != nil { 136 | return err 137 | } 138 | 139 | data, err := json.Marshal(c) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | configPath := ConfigPath() 145 | return ioutil.WriteFile(configPath, data, 0600) 146 | } 147 | 148 | func RemoveConfig() error { 149 | return os.Remove(ConfigPath()) 150 | } 151 | -------------------------------------------------------------------------------- /config/config_suite_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | 9 | "code.cloudfoundry.org/uaa-cli/config" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | func TestClient(t *testing.T) { 14 | RegisterFailHandler(Fail) 15 | RunSpecs(t, "Config Suite") 16 | } 17 | 18 | var _ = BeforeEach(func() { 19 | config.RemoveConfig() 20 | }) 21 | 22 | var _ = AfterEach(func() { 23 | config.RemoveConfig() 24 | }) 25 | 26 | func NewContextWithToken(accessToken string) config.UaaContext { 27 | ctx := config.UaaContext{ 28 | Token: oauth2.Token{ 29 | AccessToken: accessToken, 30 | }, 31 | } 32 | return ctx 33 | } 34 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package config_test 5 | 6 | import ( 7 | "code.cloudfoundry.org/uaa-cli/config" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("Config", func() { 13 | var cfg config.Config 14 | 15 | It("can read saved data", func() { 16 | cfg = config.NewConfig() 17 | 18 | target := config.NewTarget() 19 | target.BaseUrl = "http://nowhere.com" 20 | target.SkipSSLValidation = true 21 | 22 | ctx := NewContextWithToken("foo-token") 23 | ctx.ClientId = "cid" 24 | ctx.Username = "woodstock" 25 | ctx.GrantType = "client_credentials" 26 | 27 | cfg.AddTarget(target) 28 | cfg.AddContext(ctx) 29 | 30 | config.WriteConfig(cfg) 31 | 32 | Expect(cfg.ActiveTargetName).To(Equal("url:http://nowhere.com")) 33 | Expect(cfg.GetActiveContext().Token.AccessToken).To(Equal("foo-token")) 34 | cfg2 := config.ReadConfig() 35 | Expect(cfg2.ActiveTargetName).To(Equal("url:http://nowhere.com")) 36 | Expect(cfg2.GetActiveContext().Token.AccessToken).To(Equal("foo-token")) 37 | }) 38 | 39 | It("can accept a context without previously setting target", func() { 40 | cfg = config.NewConfig() 41 | ctx := NewContextWithToken("foo-token") 42 | ctx.ClientId = "cid" 43 | ctx.Username = "woodstock" 44 | ctx.GrantType = "client_credentials" 45 | cfg.AddContext(ctx) 46 | 47 | config.WriteConfig(cfg) 48 | 49 | Expect(cfg.GetActiveContext().Token.AccessToken).To(Equal("foo-token")) 50 | 51 | cfg2 := config.ReadConfig() 52 | Expect(cfg2.ActiveTargetName).To(Equal("url:")) 53 | Expect(cfg2.GetActiveContext().Token.AccessToken).To(Equal("foo-token")) 54 | }) 55 | 56 | It("places the config file in .uaa in the home directory", func() { 57 | Expect(config.ConfigPath()).To(HaveSuffix(`/.uaa/config.json`)) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /config/config_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package config 5 | 6 | import ( 7 | "os" 8 | ) 9 | 10 | func userHomeDir() string { 11 | return os.Getenv("HOME") 12 | } 13 | 14 | func makeDirectory() error { 15 | return os.MkdirAll(ConfigDir(), 0755) 16 | } 17 | -------------------------------------------------------------------------------- /config/config_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package config_test 5 | 6 | import ( 7 | "os" 8 | 9 | "path" 10 | 11 | "code.cloudfoundry.org/uaa-cli/config" 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("Config", func() { 17 | var cfg config.Config 18 | 19 | BeforeEach(func() { 20 | cfg = config.Config{} 21 | cfg.AddContext(NewContextWithToken("foo")) 22 | }) 23 | 24 | It("set appropriate permissions for persisted files", func() { 25 | config.WriteConfig(cfg) 26 | 27 | parentStat, _ := os.Stat(path.Dir(config.ConfigPath())) 28 | Expect(parentStat.Mode().String()).To(Equal("drwxr-xr-x")) 29 | 30 | fileStat, _ := os.Stat(config.ConfigPath()) 31 | Expect(fileStat.Mode().String()).To(Equal("-rw-------")) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /config/config_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package config 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | func userHomeDir() string { 12 | home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 13 | if home == "" { 14 | home = os.Getenv("USERPROFILE") 15 | } 16 | return home 17 | } 18 | 19 | func makeDirectory() error { 20 | dir := ConfigDir() 21 | 22 | err := os.MkdirAll(dir, 0755) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | p, err := syscall.UTF16PtrFromString(dir) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | attrs, err := syscall.GetFileAttributes(p) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return syscall.SetFileAttributes(p, attrs|syscall.FILE_ATTRIBUTE_HIDDEN) 38 | } 39 | -------------------------------------------------------------------------------- /config/config_win_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package config_test 5 | 6 | import ( 7 | "syscall" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("Config (windows specific)", func() { 14 | var cfg config.Config 15 | 16 | BeforeEach(func() { 17 | cfg = config.Config{ 18 | ApiURL: "http://api.example.com", 19 | AuthURL: "http://auth.example.com", 20 | } 21 | }) 22 | 23 | It("hides the config directory", func() { 24 | err := config.WriteConfig(cfg) 25 | Expect(err).NotTo(HaveOccurred()) 26 | 27 | p, err := syscall.UTF16PtrFromString(config.ConfigDir()) 28 | Expect(err).ToNot(HaveOccurred()) 29 | 30 | attrs, err := syscall.GetFileAttributes(p) 31 | Expect(err).ToNot(HaveOccurred()) 32 | 33 | Expect(attrs & syscall.FILE_ATTRIBUTE_HIDDEN).To(Equal(uint32(syscall.FILE_ATTRIBUTE_HIDDEN))) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /fixtures/builders.go: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import "encoding/json" 4 | 5 | func EntityResponse(response interface{}) string { 6 | bytes, err := json.Marshal(response) 7 | if err != nil { 8 | return "\"Failed to marshal provided entity\"" 9 | } 10 | 11 | return string(bytes) 12 | } 13 | -------------------------------------------------------------------------------- /fixtures/group_mappings_fixtures.go: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | const ExternalGroupsApiResponse = `{ 4 | "resources": [ 5 | { 6 | "displayName": "organizations.acme", 7 | "externalGroup": "cn=test_org,ou=people,o=springsource,o=org", 8 | "groupId": "59d28fbb-456c-4ab1-a1e3-0a5575ec4529", 9 | "origin": "ldap" 10 | } 11 | ], 12 | "startIndex": 1, 13 | "itemsPerPage": 1, 14 | "totalResults": 1, 15 | "schemas": [ 16 | "urn:scim:schemas:core:1.0" 17 | ] 18 | }` 19 | 20 | const ExternalGroupsApiResponseInsufficientScope = `{ 21 | "error": "insufficient_scope", 22 | "error_description": "Insufficient scope for this resource", 23 | "scope": "uaa.admin scim.read zones.uaa.admin" 24 | }` 25 | -------------------------------------------------------------------------------- /fixtures/groups.go: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | const UaaAdminGroupResponse = `{ 4 | "id" : "05a0c169-3592-4a45-b109-a16d9246e0ab", 5 | "meta" : { 6 | "version" : 1, 7 | "created" : "2017-01-15T16:54:15.677Z", 8 | "lastModified" : "2017-08-15T16:54:15.677Z" 9 | }, 10 | "displayName" : "uaa.admin", 11 | "description" : "Act as an administrator throughout the UAA", 12 | "members" : [ { 13 | "origin" : "uaa", 14 | "type" : "USER", 15 | "value" : "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70" 16 | } ], 17 | "zoneId" : "uaa", 18 | "schemas" : [ "urn:scim:schemas:core:1.0" ] 19 | }` 20 | 21 | const CloudControllerReadGroupResponse = `{ 22 | "id" : "ea777017-883e-48ba-800a-637c71409b5e", 23 | "meta" : { 24 | "version" : 1, 25 | "created" : "2017-01-15T16:54:15.677Z", 26 | "lastModified" : "2017-08-15T16:54:15.677Z" 27 | }, 28 | "displayName" : "cloud_controller.read", 29 | "description" : "View details of your applications and services", 30 | "members" : [ { 31 | "origin" : "uaa", 32 | "type" : "USER", 33 | "value" : "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70" 34 | } ], 35 | "zoneId" : "uaa", 36 | "schemas" : [ "urn:scim:schemas:core:1.0" ] 37 | }` 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module code.cloudfoundry.org/uaa-cli 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/cloudfoundry-community/go-uaa v0.3.5 7 | github.com/fatih/color v1.18.0 8 | github.com/olekukonko/tablewriter v1.0.7 9 | github.com/onsi/ginkgo/v2 v2.23.4 10 | github.com/onsi/gomega v1.37.0 11 | github.com/pkg/errors v0.9.1 12 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 13 | github.com/spf13/cobra v1.9.1 14 | golang.org/x/crypto v0.38.0 15 | golang.org/x/oauth2 v0.30.0 16 | ) 17 | 18 | require ( 19 | github.com/go-logr/logr v1.4.3 // indirect 20 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 21 | github.com/google/go-cmp v0.7.0 // indirect 22 | github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect 23 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 24 | github.com/mattn/go-colorable v0.1.14 // indirect 25 | github.com/mattn/go-isatty v0.0.20 // indirect 26 | github.com/mattn/go-runewidth v0.0.16 // indirect 27 | github.com/olekukonko/errors v1.1.0 // indirect 28 | github.com/olekukonko/ll v0.0.8 // indirect 29 | github.com/rivo/uniseg v0.4.7 // indirect 30 | github.com/spf13/pflag v1.0.6 // indirect 31 | go.uber.org/automaxprocs v1.6.0 // indirect 32 | golang.org/x/net v0.40.0 // indirect 33 | golang.org/x/sys v0.33.0 // indirect 34 | golang.org/x/term v0.32.0 // indirect 35 | golang.org/x/text v0.25.0 // indirect 36 | golang.org/x/tools v0.33.0 // indirect 37 | google.golang.org/protobuf v1.36.6 // indirect 38 | gopkg.in/yaml.v3 v3.0.1 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /help/client_credentials.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | func ClientCredentials() string { 4 | return `USAGE 5 | 6 | uaa target UAA_URL 7 | uaa get-client-credentials-token CLIENT_ID -s CLIENT_SECRET 8 | 9 | After successfully running this command, the token is added to the CLI's 10 | current context. Access tokens saved in the context will be attached to subsequent 11 | requests when attempting to use CLI commands that hit UAA endpoints requiring 12 | Authorization. 13 | 14 | BACKGROUND 15 | 16 | The Client Credentials grant type is one of the four authorization flows 17 | described in the RFC 6749. Like the other flows described in that spec, the 18 | goal of this flow is for Clients to obtain access tokens needed to perform 19 | priviledged actions against Resource Server APIs. Unlike the other flows 20 | defined in the OAuth2 spec, the client_credentials grant type is used by 21 | Clients performing actions on their own behalf without the involvement of a 22 | User. 23 | 24 | +---------+ +---------------+ 25 | | | | | 26 | | |>--(A)- Client Authentication --->| Authorization | 27 | | Client | | Server | 28 | | |<--(B)---- Access Token ---------<| | 29 | | | | | 30 | +---------+ +---------------+ 31 | 32 | The get-client-credentials-token command allows you to test your client 33 | registration. By providing your client_id and client_secret to this CLI, it 34 | is able to play the part of your Client application in the flow. 35 | 36 | WHEN SHOULD PASSWORD GRANT CLIENTS BE USED 37 | 38 | The client_credentials flow is typically used by Client applications wanting 39 | to perform service-to-service operations on its own behalf against a Resource 40 | Server. Many components within Cloud Foundry leaverage the client_credentials 41 | grant type to secure their service-to-service interactions. 42 | 43 | For example, if your client is a web application that need to send out a 44 | weekly email newsletter, it might make scheduled calls to an 45 | email/notifications service with a client_credentials token. In this example, 46 | the client_credentials grant type is appropriate because the client is calling 47 | the notification service on its own behalf and the resources it is accessing 48 | are not owned in any meaningful sense by the user. 49 | 50 | TROUBLESHOOTING FAQ 51 | 52 | Scenario: You are unable to get a token using get-client-credentials-token. 53 | 54 | - Ensure you are using valid client_id and client_secret for the targeted 55 | UAA. If you are very confident in these values, double check that you are 56 | targetting the correct UAA. 57 | 58 | - Ensure that "client_credentials" is included in the list of 59 | authorized_grant_types for your client. This flow may only be used with 60 | clients that have registered for the client_credentials grant type. 61 | 62 | Scenario: You got a token but it does not have the expected scopes. 63 | 64 | - Verify the "authorities" field of your client registration includes the 65 | scopes you want to be included in the token. The "scope" field of your 66 | client registration is not considered by the UAA when authoring tokens for 67 | the client_credentials grant type. 68 | ` 69 | } 70 | -------------------------------------------------------------------------------- /help/clients.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | func CreateClient() string { 4 | return ` 5 | Clients wishing to obtain tokens representing a user's delegated authorization 6 | must perform a one-time registration with the UAA before initiating any OAuth 7 | flows. There are many configurable properties on clients. The most important 8 | ones are the selection of the authorized grant types for your client. 9 | Depending on the grant type you choose other properties may or may not be 10 | required. 11 | 12 | Most errors that occur during create-client can be easily diagnosed by using 13 | the --verbose option to inspect the error response sent from the UAA. 14 | 15 | EXAMPLE USAGE 16 | 17 | uaa create-client shinymail \ 18 | --client_secret secret \ 19 | --authorized_grant_types authorization_code \ 20 | --redirect_uri http://localhost:9090/*,http://email-sass.om/callback \ 21 | --scope mail.send,mail.read \ 22 | --display_name "Shinymail Web Mail Reader" 23 | 24 | uaa create-client background_emailer \ 25 | --client_secret secret \ 26 | --authorized_grant_types client_credentials \ 27 | --authorities notifications.write \ 28 | --display_name "Weekly newsletter email service" 29 | 30 | uaa create-client single_page_todo_app \ 31 | --authorized_grant_types implicit \ 32 | --redirect_uri http://localhost:9090/*,http://reactapp.com/callback \ 33 | --scope todo.read,todo.write \ 34 | --display_name "A Single-Page Todo App" 35 | 36 | uaa create-client trusted_cli \ 37 | --client_secret mumstheword \ 38 | --authorized_grant_types password \ 39 | --scope cloud_controller.admin,uaa.admin,network.write 40 | 41 | uaa create-client trusted_cli_copy \ 42 | --clone trusted_cli \ 43 | --client_secret donttellanyone 44 | ` 45 | } 46 | -------------------------------------------------------------------------------- /help/context.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | func Context() string { 4 | return `A context represents a previously fetched access token and associated metadata 5 | such as the scopes that token contains. The uaa CLI caches these results on a 6 | local file so that they may be used when issuing requests that require an 7 | Authorization header. 8 | ` 9 | } 10 | -------------------------------------------------------------------------------- /help/list_users.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | func ListUsers() string { 4 | return `SCIM is the System for Cross-Domain Identity Management. The standard describes 5 | a format, or schema, for representing identity information and specifies the API for 6 | accessing that data. 7 | 8 | Of particular interest for this command are the SCIM filters. SCIM filters 9 | provide a limited query interface. See examples below for usage. 10 | 11 | Examples: 12 | 13 | - Show all usernames containing gmail.com: 14 | uaa list-users --filter 'userName co "gmail.com"' --attributes id,emails 15 | 16 | - Show all users from a particular origin (identity provider): 17 | uaa list-users --filter 'userName eq "bob@example.com" and origin eq "ldap"' 18 | 19 | - Find all unverified users: 20 | uaa list-users --filter 'verified eq false' --attributes id,userName,name,emails 21 | 22 | - Find users whose username starts with "z": 23 | uaa list-users --filter 'userName sw "z"' 24 | 25 | - See client approvals for a specific user: 26 | uaa list-users --filter 'userName eq "bob@example.com' --attributes approvals 27 | 28 | - See everything about a specific user, including group memberships: 29 | uaa list-users --filter 'userName eq "bob@example.com' 30 | 31 | Keep in mind that the UAA must perform SQL joins to determine group membership so 32 | responses will be relatively slow when fetching results for large numbers of users 33 | without filtering to specific attributes of interest with the --attributes flag. 34 | ` 35 | } 36 | -------------------------------------------------------------------------------- /help/password_grant.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | func PasswordGrant() string { 4 | return `USAGE 5 | 6 | uaa target UAA_URL 7 | uaa get-password-token CLIENT_ID -s CLIENT_SECRET -u USERNAME -p PASSWORD 8 | 9 | After successfully running this command, the token is added to the CLI's 10 | current context. Access tokens saved in the context will be attached to subsequent 11 | requests when attempting to use CLI commands that hit UAA endpoints requiring 12 | Authorization. 13 | 14 | BACKGROUND 15 | 16 | The "Resource Owner Password Credentials" grant type, often referred to by the 17 | shorter name "password grant", is one of the four authorization flows 18 | described in the RFC 6749. Like the other flows described in that spec, the 19 | goal of this flow is to obtain access tokens for a Client application that 20 | represent the delegated authorization of a User. The Client may then present 21 | the token to a Resource Service in an Authorization header to perform 22 | priviledged action on the User's behalf. 23 | 24 | +----------+ 25 | | Resource | 26 | | Owner | 27 | | (User) | 28 | +----------+ 29 | v 30 | | Resource Owner 31 | (A) Password Credentials 32 | | (i.e. username and password) 33 | v 34 | +----------+ +---------------+ 35 | | |>--(B)---- Resource Owner ------->| | 36 | | | Password Credentials | Authorization | 37 | | Client | | Server | 38 | |(this CLI)|<--(C)---- Access Token ---------<| (UAA) | 39 | | | (w/ Optional Refresh Token) | | 40 | +----------+ +---------------+ 41 | 42 | 43 | The get-password-token command allows you to test your client registration. 44 | By providing your client_id and client_secret to this CLI, it is able to 45 | play the part of your Client application in the Resource Owner Password 46 | Credentials flow. 47 | 48 | WHEN SHOULD PASSWORD GRANT CLIENTS BE USED 49 | 50 | This flow is typically only used for Client applications that are considered 51 | highly trusted by the User, since they must provide their username and 52 | password directly to the Client who will use them on their behalf. CLIs or 53 | other native applications running on user-owned hardware are good candidates 54 | for the password grant flow. 55 | 56 | The Cloud Foundry CLI and the Credhub CLI are two examples of password grant 57 | clients within the CF ecosystem. 58 | 59 | TROUBLESHOOTING FAQ 60 | 61 | Scenario: You are unable to get a token using get-password-token. 62 | 63 | - Ensure you are using valid client_id, client_secret, username, and password. 64 | If you are very confident in these values, run with --verbose and make sure 65 | you have targeted the correct UAA for the credentials you are passing. 66 | 67 | - Ensure that "password" in the list of authorized_grant_types for your client. 68 | 69 | Scenario: You got a token but it does not have the expected scopes. 70 | 71 | - Verify "scope" field of your client registration includes the scopes you 72 | want to be included in the token. 73 | 74 | - Verify the user whose username and password you are using has the correct 75 | group memberships. The scopes in the issued token will be the intersection 76 | of the scopes your client requests (by listing them in the client 77 | registration) and the group memberships, or permissions, that the user 78 | has. 79 | ` 80 | } 81 | -------------------------------------------------------------------------------- /help/refresh_token.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | func RefreshToken() string { 4 | return `USAGE 5 | 6 | uaa target UAA_URL 7 | uaa get-password-token CLIENT_ID -s CLIENT_SECRET -u USERNAME -p PASSWORD 8 | uaa context 9 | uaa refresh-token -s CLIENT_SECRET 10 | uaa context # the access_token should now be updated 11 | 12 | The refresh-token command is used by authorization_code and password clients 13 | to obtain a new, unexpired access_token token from the UAA. Refresh tokens are 14 | long-lived and should be kept confidential by clients. 15 | 16 | TROUBLESHOOTING FAQ 17 | 18 | Scenario: You do not have refresh_token in your active context. 19 | 20 | - If your context shows grant_type implicit: Implicit Clients are unable to 21 | maintain secrets and for that reason are not issued refresh_tokens. 22 | 23 | - If your context shows grant_type authorization_code: Clients using the 24 | authorization_code grant type are only issued refresh tokens if they are 25 | also registered with the refresh_token in the authorized_grant_types list. 26 | Use an administrative client to view your client registration with "uaa 27 | get-client" and ensure refresh_token is in the authorized_grant_types 28 | list. 29 | 30 | - If your context shows grant_type password: Clients using the password 31 | grant type are only issued refresh tokens if they are also configured with 32 | the refresh_token in the authorized_grant_types list. Use an 33 | administrative client to view your client registration with "uaa 34 | get-client" and ensure refresh_token is in the authorized_grant_types 35 | list. 36 | 37 | - If your context shows grant_type client_credentials: Refresh tokens are 38 | never issued when using the client_credentials grant flow. Refresh tokens 39 | are used to get a renewed access_token representing a user's delegated 40 | authorization without the need for a user to reauthorize. With 41 | client_credentials grant type, there is no user and so you may obtain 42 | another token at any time using your client_id and client_secret with the 43 | client_credentials grant flow. 44 | ` 45 | } 46 | -------------------------------------------------------------------------------- /help/root.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | import "fmt" 4 | 5 | func Root(version string) string { 6 | return fmt.Sprintf(`UAA Command Line Interface, version %v 7 | 8 | Feedback: 9 | Email cf-identity-eng@pivotal.io with your thoughts on the experience of using this 10 | tool. Bugs or other issues can be filed on github.com/cloudfoundry-incubator/uaa-cli 11 | `, version) 12 | } 13 | -------------------------------------------------------------------------------- /help/userinfo.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | func Userinfo() string { 4 | return ` With a valid user token saved in context, use the me command to see 5 | information about the authenticated user. 6 | 7 | uaa target UAA_URL 8 | uaa get-password-token CLIENT_ID -s CLIENT_SECRET -u USERNAME -p PASSWORD 9 | uaa me 10 | 11 | BACKGROUND 12 | 13 | The me command calls the UAA's /userinfo endpoint. This endpoint has been 14 | implemented in accordance with the OIDC 1.1 spec. 15 | 16 | The /userinfo endpoint is an OAuth 2.0 protected resource that returns claims 17 | about the authenticated user. To obtain the requested claims about the 18 | user, the client makes a request to the /userinfo endpoint using an access 19 | token obtained through OpenID Connect Authentication. 20 | 21 | TROUBLESHOOTING FAQ 22 | 23 | Scenario: You have obtained a token but are unable to see results from the 24 | "me" command. 25 | 26 | - Verify your token is still valid by using another command that requires 27 | authentication. If it is no longer valid, request another token and try 28 | again. 29 | 30 | - Check whether the output of "uaa context" shows "openid" in the list of 31 | scopes. This scope is required to access the /userinfo endpoint and it won't 32 | appear in your token unless requested by the client. You may need to update 33 | your client registration to include "openid" in the scope list. 34 | ` 35 | } 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 jhamon@gmail.com 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import "code.cloudfoundry.org/uaa-cli/cmd" 24 | 25 | func main() { 26 | cmd.Execute() 27 | } 28 | -------------------------------------------------------------------------------- /utils/arrayify.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | const ( 6 | commaSpace = ", " 7 | commaOnly = "," 8 | spaceOnly = " " 9 | ) 10 | 11 | func Arrayify(input string) []string { 12 | input = strings.TrimSpace(input) 13 | 14 | if input == "" { 15 | return []string{} 16 | } else if strings.Contains(input, commaSpace) { 17 | return removeEmpty(strings.Split(input, commaSpace)) 18 | } else if strings.Contains(input, commaOnly) { 19 | return removeEmpty(strings.Split(input, commaOnly)) 20 | } else if strings.Contains(input, spaceOnly) { 21 | return removeEmpty(strings.Split(input, spaceOnly)) 22 | } 23 | return []string{input} 24 | } 25 | 26 | func removeEmpty(input []string) []string { 27 | output := []string{} 28 | for _, item := range input { 29 | if item != "" { 30 | output = append(output, strings.TrimSpace(item)) 31 | } 32 | } 33 | return output 34 | } 35 | -------------------------------------------------------------------------------- /utils/arrayify_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | . "code.cloudfoundry.org/uaa-cli/utils" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("Arrayify", func() { 11 | It("Can handle whitespace", func() { 12 | Expect(Arrayify("")).To(Equal([]string{})) 13 | Expect(Arrayify(" ")).To(Equal([]string{})) 14 | Expect(Arrayify(" ")).To(Equal([]string{})) 15 | }) 16 | 17 | It("Can handle a single item", func() { 18 | Expect(Arrayify("foo")).To(Equal([]string{"foo"})) 19 | Expect(Arrayify("foo ")).To(Equal([]string{"foo"})) 20 | }) 21 | 22 | It("Can split comma-separated string into slice of strings", func() { 23 | Expect(Arrayify("foo,bar,baz")).To(Equal([]string{"foo", "bar", "baz"})) 24 | }) 25 | 26 | It("Can split space-separated string into slice of strings", func() { 27 | Expect(Arrayify("foo bar baz")).To(Equal([]string{"foo", "bar", "baz"})) 28 | }) 29 | 30 | It("Can split comma-space-separated string into slice of strings", func() { 31 | Expect(Arrayify("foo, bar, baz")).To(Equal([]string{"foo", "bar", "baz"})) 32 | }) 33 | 34 | It("Can trim extra whitespace", func() { 35 | Expect(Arrayify(" foo bar baz ")).To(Equal([]string{"foo", "bar", "baz"})) 36 | Expect(Arrayify("foo,bar,baz ")).To(Equal([]string{"foo", "bar", "baz"})) 37 | }) 38 | 39 | It("Can trim extra spaces between entries", func() { 40 | Expect(Arrayify("foo bar baz")).To(Equal([]string{"foo", "bar", "baz"})) 41 | Expect(Arrayify("foo, bar, baz")).To(Equal([]string{"foo", "bar", "baz"})) 42 | }) 43 | 44 | It("Can remove empty entries", func() { 45 | Expect(Arrayify("foo,,bar,baz")).To(Equal([]string{"foo", "bar", "baz"})) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /utils/boolean_pointers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func NewTrueP() *bool { 4 | b := true 5 | return &b 6 | } 7 | 8 | func NewFalseP() *bool { 9 | b := false 10 | return &b 11 | } 12 | -------------------------------------------------------------------------------- /utils/contains.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func Contains(slice []string, toFind string) bool { 4 | for _, a := range slice { 5 | if a == toFind { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /utils/contains_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | . "code.cloudfoundry.org/uaa-cli/utils" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("Contains", func() { 11 | list := []string{"do", "re", "mi"} 12 | 13 | It("returns true if present", func() { 14 | Expect(Contains(list, "re")).To(BeTrue()) 15 | }) 16 | 17 | It("returns false if not present", func() { 18 | Expect(Contains(list, "fa")).To(BeFalse()) 19 | }) 20 | 21 | It("handles empty list", func() { 22 | Expect(Contains([]string{}, "fa")).To(BeFalse()) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /utils/stringifier.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | func StringSliceStringifier(stringsList []string) string { 6 | return "[" + strings.Join(stringsList, ", ") + "]" 7 | } 8 | -------------------------------------------------------------------------------- /utils/stringifier_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | . "code.cloudfoundry.org/uaa-cli/utils" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("Stringifier", func() { 11 | It("presents a string slice as a string", func() { 12 | Expect(StringSliceStringifier([]string{"foo", "bar", "baz"})).To(Equal("[foo, bar, baz]")) 13 | Expect(StringSliceStringifier([]string{"foo"})).To(Equal("[foo]")) 14 | Expect(StringSliceStringifier([]string{})).To(Equal("[]")) 15 | Expect(StringSliceStringifier([]string{" "})).To(Equal("[ ]")) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /utils/styling.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/fatih/color" 4 | 5 | var Emphasize = color.New(color.FgCyan, color.Bold).SprintFunc() 6 | var Red = color.New(color.FgRed).SprintFunc() 7 | var Green = color.New(color.FgGreen).SprintFunc() 8 | -------------------------------------------------------------------------------- /utils/url_helpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/url" 5 | "path" 6 | ) 7 | 8 | func BuildUrl(baseUrl, newPath string) (*url.URL, error) { 9 | newUrl, err := url.Parse(baseUrl) 10 | if err != nil { 11 | return nil, err 12 | } 13 | 14 | newUrl.Path = path.Join(newUrl.Path, newPath) 15 | return newUrl, nil 16 | } 17 | -------------------------------------------------------------------------------- /utils/url_helpers_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/uaa-cli/utils" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("UrlHelpers", func() { 10 | Describe("BuildUrl", func() { 11 | It("adds path to base url", func() { 12 | url, _ := utils.BuildUrl("http://localhost:9090", "foo") 13 | Expect(url.String()).To(Equal("http://localhost:9090/foo")) 14 | 15 | url, _ = utils.BuildUrl("http://localhost:9090/", "foo") 16 | Expect(url.String()).To(Equal("http://localhost:9090/foo")) 17 | 18 | url, _ = utils.BuildUrl("http://localhost:9090/", "/foo") 19 | Expect(url.String()).To(Equal("http://localhost:9090/foo")) 20 | 21 | url, _ = utils.BuildUrl("http://localhost:9090", "/foo") 22 | Expect(url.String()).To(Equal("http://localhost:9090/foo")) 23 | }) 24 | 25 | It("preserves the base path", func() { 26 | url, _ := utils.BuildUrl("http://localhost:9090/uaa", "foo") 27 | Expect(url.String()).To(Equal("http://localhost:9090/uaa/foo")) 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /utils/utils_suite_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestUtils(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Utils Suite") 13 | } 14 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var Version string 4 | var Commit string 5 | 6 | func VersionString() string { 7 | return Version + " " + Commit 8 | } 9 | --------------------------------------------------------------------------------