Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: UAA integration test

# Exercises the compiled uaa-cli binary against a real UAA server (not a
# mocked HTTP server) so that response-shape bugs -- e.g. go-uaa failing to
# unmarshal a field UAA legitimately serializes as more than one JSON type --
# are caught before merge instead of by a user in production.
#
# Known gaps (not covered here): get-authcode-token and get-implicit-token
# need a real browser + local OAuth redirect callback, which isn't reliably
# scriptable headlessly. Zone-scoped (-z/--zone) command variants are also
# not exercised.
#
# Currently disabled (integration/clients_test.go): the spec that actually
# reproduces the original list-clients/allowpublic regression is marked
# PIt, because it will fail until cloudfoundry-community/go-uaa#175 is
# released and go.mod is bumped to it -- there's no released go-uaa version
# with the fix yet. Un-pend it once that lands; until then, this workflow
# exercises everything else but not that specific regression.

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '20 7 * * *'
workflow_dispatch:

permissions:
contents: read

jobs:
uaa-integration-test:
runs-on: ubuntu-latest
steps:
- name: Checkout uaa-cli
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Checkout cloudfoundry/uaa
uses: actions/checkout@v7
with:
repository: cloudfoundry/uaa
ref: develop
Comment thread
duanemay marked this conversation as resolved.
path: uaa
persist-credentials: false

- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '25'

- name: Cache Gradle
uses: actions/cache@v6
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('uaa/**/*.gradle*', 'uaa/gradle/wrapper/gradle-wrapper.properties') }}

- name: Start UAA
run: scripts/start-uaa.sh uaa

- uses: actions/setup-go@v7
with:
go-version-file: go.mod
check-latest: true

- name: Run live UAA integration suite
run: make test-integration

- name: Dump UAA boot log on failure
if: failure()
run: cat uaa-boot.log || true
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ uaa-cli
uaa
build/
dist/
uaa-boot.log
uaa-boot.pid
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ format: ## Format Go Code

.PHONY: test
test: ## Run Ginkgo tests
go run github.com/onsi/ginkgo/v2/ginkgo -v -r --randomize-suites --randomize-all -race
go run github.com/onsi/ginkgo/v2/ginkgo -v -r --randomize-suites --randomize-all -race --skip-package=integration

.PHONY: test-integration
test-integration: ## Run the live-UAA integration suite (requires a running UAA at $$UAA_TARGET, default http://localhost:8080/uaa)
go run github.com/onsi/ginkgo/v2/ginkgo -tags integration -v ./integration/...

.PHONY: test-all
test-all: test test-integration ## Run both the unit and live-UAA integration suites

.PHONY: goreleaser-check
goreleaser-check: ## Test goreleaser configuration
Expand Down
8 changes: 4 additions & 4 deletions cmd/refresh_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ func RefreshTokenValidations(cfg config.Config, clientSecret string) error {
if err := cli.EnsureContextInConfig(cfg); err != nil {
return err
}
if clientSecret == "" {
return cli.MissingArgumentError("client_secret")
}
// client_secret is intentionally not required here: public clients (e.g.
// UAA's built-in "cf" client) have no secret, and get-password-token
// already allows an empty client_secret for the same reason.
if cfg.GetActiveContext().ClientId == "" {
Comment thread
duanemay marked this conversation as resolved.
return errors.New("A client_id was not found in the active context.")
}
Expand All @@ -68,7 +68,7 @@ func RefreshTokenValidations(cfg config.Config, clientSecret string) error {
}

var refreshTokenCmd = &cobra.Command{
Use: "refresh-token -s CLIENT_SECRET",
Use: "refresh-token [-s CLIENT_SECRET]",
Short: "Obtain an access token using the refresh_token grant type",
Long: help.RefreshToken(),
PreRun: func(cmd *cobra.Command, args []string) {
Expand Down
28 changes: 24 additions & 4 deletions cmd/refresh_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,19 +167,39 @@ var _ = Describe("ResfrehToken", func() {

Describe("Validations", func() {
Describe("when called with no client_secret", func() {
It("displays help and does not panic", func() {
ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
// Public clients (e.g. UAA's built-in "cf" client) have no secret,
// so an empty/omitted client_secret must be allowed here, matching
// get-password-token's handling of the same case.
BeforeEach(func() {
c = config.NewConfigWithServerURL(server.URL())
ctx = config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
ctx.GrantType = config.PASSWORD
ctx.Token.RefreshToken = "refresh me"
ctx.ClientId = "shinyclient"
ctx.Username = "woodstock"
c.AddContext(ctx)
config.WriteConfig(c)

server.RouteToHandler("POST", "/oauth/token", CombineHandlers(
RespondWith(http.StatusOK, jwtTokenResponseJson, contentTypeJson),
VerifyHeaderKV("Authorization", "Basic c2hpbnljbGllbnQ6"),
VerifyFormKV("refresh_token", "refresh me"),
VerifyFormKV("grant_type", "refresh_token"),
))
})

It("succeeds with an explicitly empty client_secret", func() {
session := runCommand("refresh-token", "-s", "")

Eventually(session).Should(Exit(0))
Eventually(session).Should(Say("Access token successfully fetched and added to active context."))
})

It("succeeds with client_secret omitted entirely", func() {
session := runCommand("refresh-token")

Eventually(session).Should(Exit(1))
Expect(session.Err).To(Say("Missing argument `client_secret` must be specified."))
Eventually(session).Should(Exit(0))
Eventually(session).Should(Say("Access token successfully fetched and added to active context."))
})
})

Expand Down
5 changes: 3 additions & 2 deletions help/refresh_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ func RefreshToken() string {
uaa target UAA_URL
uaa get-password-token CLIENT_ID -s CLIENT_SECRET -u USERNAME -p PASSWORD
uaa context
uaa refresh-token -s CLIENT_SECRET
uaa refresh-token [-s CLIENT_SECRET]
uaa context # the access_token should now be updated

The refresh-token command is used by authorization_code and password clients
to obtain a new, unexpired access_token token from the UAA. Refresh tokens are
long-lived and should be kept confidential by clients.
long-lived and should be kept confidential by clients. CLIENT_SECRET may be
omitted (or passed as an empty string) for public clients that have none.

TROUBLESHOOTING FAQ

Expand Down
25 changes: 25 additions & 0 deletions integration/basics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build integration

package integration

import (
. "github.com/onsi/ginkgo/v2"
)

var _ = Describe("cli basics", func() {
It("shows server info", func() {
assertValidJSON(runOK("info"))
})

It("shows the current context", func() {
runOK("context")
})

It("lists saved contexts", func() {
runOK("contexts")
})

It("prints the version", func() {
runOK("version")
})
})
64 changes: 64 additions & 0 deletions integration/clients_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//go:build integration

package integration

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("clients", Ordered, func() {
clientID := "uaa-cli-integration-test-client"

AfterAll(func() {
// Other Describes assume an admin token is active; assert this one.
runOK("get-client-credentials-token", "admin", "-s", "adminsecret")
run("delete-client", clientID)
})

PIt("lists clients, including the legacy string-typed additionalInformation clients seeded by scripts/boot/uaa.yml", func() {
// login, client_federated_jwt_trust, client_with_allowpublic_and_jwks_uri_trust,
// and oauth_showcase_saml2_bearer are all seeded with allowpublic/autoapprove as
// YAML strings, which UAA stores verbatim and serializes back as JSON strings
// rather than booleans -- the exact shape that broke `uaa list-clients` originally.
Comment on lines +19 to +23

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chagne to It after update to new fixed go-uaa

// Un-pend once cloudfoundry-community/go-uaa#175 is released and go.mod is bumped.
session := runOK("list-clients")
assertValidJSON(session)
Expect(string(session.Out.Contents())).To(ContainSubstring("client_federated_jwt_trust"))
})

It("gets one of the legacy string-typed clients directly", func() {
// client_federated_jwt_trust has no allowpublic key at all, so unlike
// list-clients above this doesn't hit the go-uaa bug -- it's here to
// confirm get-client for other legacy clients still works today.
assertValidJSON(runOK("get-client", "client_federated_jwt_trust"))
})

It("creates a client", func() {
runOK("create-client", clientID,
"-s", "test-secret",
"--authorized_grant_types", "client_credentials,refresh_token",
"--scope", "uaa.none",
// clients.secret is required for a client to change its own secret
// via change-client-secret below.
"--authorities", "uaa.none,clients.secret",
)
})

It("gets the new client", func() {
assertValidJSON(runOK("get-client", clientID))
})

It("updates the client", func() {
runOK("update-client", clientID, "--scope", "uaa.none,openid")
})

It("sets the client secret", func() {
runOK("set-client-secret", clientID, "-s", "second-test-secret")
})

It("gets a token as the client and changes its own secret", func() {
runOK("get-client-credentials-token", clientID, "-s", "second-test-secret")
runOK("change-client-secret", "--old_secret", "second-test-secret", "--secret", "third-test-secret")
})
})
52 changes: 52 additions & 0 deletions integration/groups_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//go:build integration

package integration

import (
. "github.com/onsi/ginkgo/v2"
)

var _ = Describe("groups", Ordered, func() {
groupName := "uaa.cli.integration.test.group"
memberUsername := "uaa-cli-integration-test-group-member"

BeforeAll(func() {
runOK("create-user", memberUsername,
"--givenName", "Group", "--familyName", "Member",
"--email", memberUsername+"@example.com", "-p", "S0meSecur3Pass!",
)
})

AfterAll(func() {
run("delete-user", memberUsername)
// uaa-cli has no delete-group command yet, so clean up via the raw
// SCIM endpoint to avoid a name collision if this suite runs again
// against the same (non-freshly-booted) UAA instance.
if id := groupID(groupName); id != "" {
run("curl", "-X", "DELETE", "/Groups/"+id)
}
})

It("creates a group", func() {
runOK("create-group", groupName, "-d", "uaa-cli integration test group")
})

It("gets the group", func() {
assertValidJSON(runOK("get-group", groupName))
})

It("lists groups", func() {
assertValidJSON(runOK("list-groups"))
})

It("adds and removes a member", func() {
runOK("add-member", groupName, memberUsername)
runOK("remove-member", groupName, memberUsername)
})

It("maps and unmaps an external group", func() {
runOK("map-group", "cn=integration-test,ou=groups,dc=example,dc=com", groupName)
assertValidJSON(runOK("list-group-mappings"))
runOK("unmap-group", "cn=integration-test,ou=groups,dc=example,dc=com", groupName)
})
})
19 changes: 19 additions & 0 deletions integration/misc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build integration

package integration

import (
. "github.com/onsi/ginkgo/v2"
)

var _ = Describe("misc", func() {
It("curls an arbitrary endpoint", func() {
assertValidJSON(runOK("curl", "/info"))
})

It("gets userinfo for a user-context token", func() {
runOK("get-password-token", "cf", "-s", "", "-u", "marissa", "-p", "koala")
assertValidJSON(runOK("userinfo"))
runOK("get-client-credentials-token", "admin", "-s", "adminsecret")
})
})
Loading