Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added authentication token caching #51

Merged
merged 4 commits into from
Jun 18, 2024
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
4 changes: 2 additions & 2 deletions .local/getcerts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ cd "$(dirname "$0")"

mkdir -p ssl

kubectl get secret cf-service-operator-webhook -o jsonpath='{.data.tls\.key}' | base64 -d > ssl/tls.key
kubectl get secret cf-service-operator-webhook -o jsonpath='{.data.tls\.crt}' | base64 -d > ssl/tls.crt
kubectl get secret cf-service-operator-tls -o jsonpath='{.data.tls\.key}' | base64 -d > ssl/tls.key
kubectl get secret cf-service-operator-tls -o jsonpath='{.data.tls\.crt}' | base64 -d > ssl/tls.crt
66 changes: 63 additions & 3 deletions internal/cf/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package cf

import (
"fmt"
"sync"

cfclient "github.com/cloudfoundry-community/go-cfclient/v3/client"
cfconfig "github.com/cloudfoundry-community/go-cfclient/v3/config"
Expand All @@ -33,6 +34,11 @@ type organizationClient struct {
client cfclient.Client
}

type clientIdentifier struct {
URL string
Username string
}

type spaceClient struct {
url string
username string
Expand Down Expand Up @@ -89,14 +95,68 @@ func newSpaceClient(spaceGuid string, url string, username string, password stri
return &spaceClient{url: url, username: username, password: password, spaceGuid: spaceGuid, client: *c}, nil
}

var (
spaceClientCache = make(map[clientIdentifier]*spaceClient)
orgClientCache = make(map[clientIdentifier]*organizationClient)
cacheMutex = &sync.Mutex{}
)

func NewOrganizationClient(organizationName string, url string, username string, password string) (facade.OrganizationClient, error) {
return newOrganizationClient(organizationName, url, username, password)
cacheMutex.Lock()
defer cacheMutex.Unlock()
identifier := clientIdentifier{URL: url, Username: username}
client, cached := orgClientCache[identifier]
var err error
if !cached {
client, err = newOrganizationClient(organizationName, url, username, password)
if err == nil {
orgClientCache[identifier] = client
}
} else {
// If the password has changed since we cached the client, we want to update it to the new one
if client.password != password {
client.password = password
}
}
return client, err
}

func NewSpaceClient(spaceGuid string, url string, username string, password string) (facade.SpaceClient, error) {
return newSpaceClient(spaceGuid, url, username, password)
cacheMutex.Lock()
defer cacheMutex.Unlock()
identifier := clientIdentifier{URL: url, Username: username}
client, cached := spaceClientCache[identifier]
var err error
if !cached {
client, err = newSpaceClient(spaceGuid, url, username, password)
if err == nil {
spaceClientCache[identifier] = client
}
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we cover the password rotation for the OrgClient as well?

// If the password has changed since we cached the client, we want to update it to the new one
if client.password != password {
client.password = password
}
}
return client, err
}

func NewSpaceHealthChecker(spaceGuid string, url string, username string, password string) (facade.SpaceHealthChecker, error) {
return newSpaceClient(spaceGuid, url, username, password)
cacheMutex.Lock()
defer cacheMutex.Unlock()
identifier := clientIdentifier{URL: url, Username: username}
client, cached := spaceClientCache[identifier]
var err error
if !cached {
client, err = newSpaceClient(spaceGuid, url, username, password)
if err == nil {
spaceClientCache[identifier] = client
}
} else {
// If the password has changed since we cached the client, we want to update it to the new one
if client.password != password {
client.password = password
}
}
return client, err
}
234 changes: 234 additions & 0 deletions internal/cf/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/*
SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cf-service-operator contributors
SPDX-License-Identifier: Apache-2.0
*/
package cf

import (
"context"
"testing"
"time"

cfResource "github.com/cloudfoundry-community/go-cfclient/v3/resource"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/ghttp"
)

// constants useful for this file
// Note:
// - if constants are used in multiple controllers, consider moving them to suite_test.go
// - use separate resource names to prevent collisions between tests

const (
OrgName = "test-org"
Username = "testUser"
Password = "testPass"
Owner = "testOwner"

spacesURI = "/v3/spaces"
serviceInstancesURI = "/v3/service_instances"
uaaURI = "/uaa/oauth/token"
)

type Token struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
Expiry time.Time `json:"expiry,omitempty"`
}

func TestCFClient(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "CF Client Test Suite")
}

// -----------------------------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------------------------

var _ = Describe("CF Client tests", Ordered, func() {
var server *ghttp.Server
var url string
var rootResult cfResource.Root
var statusCode int
var ctx context.Context
var tokenResult Token

BeforeAll(func() {
ctx = context.Background()
// Setup fake server
server = ghttp.NewServer()
url = "http://" + server.Addr()
statusCode = 200
rootResult = cfResource.Root{
Links: cfResource.RootLinks{
Uaa: cfResource.Link{
Href: url + "/uaa",
},
Login: cfResource.Link{
Href: url + "/login",
},
},
}
tokenResult = Token{
AccessToken: "Foo",
TokenType: "Bar",
RefreshToken: "Baz",
Expiry: time.Now().Add(time.Minute),
}
By("creating space CR")
})
AfterAll(func() {
// Shutdown the server after tests
server.Close()
})

Describe("NewOrganizationClient", func() {
BeforeEach(func() {
// Reset the cache so tests can be run independently
orgClientCache = make(map[clientIdentifier]*organizationClient)
// Reset server call counts
server.Reset()
// Register handlers
server.RouteToHandler("GET", "/", ghttp.CombineHandlers(
ghttp.RespondWithJSONEncodedPtr(&statusCode, &rootResult),
))
server.RouteToHandler("GET", spacesURI, ghttp.CombineHandlers(
ghttp.RespondWithJSONEncodedPtr(&statusCode, &rootResult),
))
server.RouteToHandler("POST", uaaURI, ghttp.CombineHandlers(
ghttp.RespondWithJSONEncodedPtr(&statusCode, &tokenResult),
))
})

It("should create OrgClient", func() {
NewOrganizationClient(OrgName, url, Username, Password)

// Discover UAA endpoint
Expect(server.ReceivedRequests()[0].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/"))

Expect(server.ReceivedRequests()).To(HaveLen(1))
})

It("should be able to query some org", func() {
orgClient, err := NewOrganizationClient(OrgName, url, Username, Password)
Expect(err).To(BeNil())

orgClient.GetSpace(ctx, Owner)

// Discover UAA endpoint
Expect(server.ReceivedRequests()[0].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/"))
// Get new oAuth token
Expect(server.ReceivedRequests()[1].Method).To(Equal("POST"))
Expect(server.ReceivedRequests()[1].URL.Path).To(Equal(uaaURI))
// Get space
Expect(server.ReceivedRequests()[2].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[2].URL.Path).To(Equal(spacesURI))

Expect(server.ReceivedRequests()).To(HaveLen(3))
})

It("should be able to query some org twice", func() {
orgClient, err := NewOrganizationClient(OrgName, url, Username, Password)
Expect(err).To(BeNil())

orgClient.GetSpace(ctx, Owner)
orgClient, err = NewOrganizationClient(OrgName, url, Username, Password)
Expect(err).To(BeNil())
orgClient.GetSpace(ctx, Owner)

// Discover UAA endpoint
Expect(server.ReceivedRequests()[0].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/"))
// Get new oAuth token
Expect(server.ReceivedRequests()[1].Method).To(Equal("POST"))
Expect(server.ReceivedRequests()[1].URL.Path).To(Equal(uaaURI))
// Get space
Expect(server.ReceivedRequests()[2].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[2].URL.Path).To(Equal(spacesURI))

// Get space
Expect(server.ReceivedRequests()[3].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[3].URL.Path).To(Equal(spacesURI))

Expect(server.ReceivedRequests()).To(HaveLen(4))
})
})

Describe("NewSpaceClient", func() {
BeforeEach(func() {
// Reset the cache so tests can be run independently
spaceClientCache = make(map[clientIdentifier]*spaceClient)
// Reset server call counts
server.Reset()
// Register handlers
server.RouteToHandler("GET", "/", ghttp.CombineHandlers(
ghttp.RespondWithJSONEncodedPtr(&statusCode, &rootResult),
))
server.RouteToHandler("GET", serviceInstancesURI, ghttp.CombineHandlers(
ghttp.RespondWithJSONEncodedPtr(&statusCode, &rootResult),
))
server.RouteToHandler("POST", uaaURI, ghttp.CombineHandlers(
ghttp.RespondWithJSONEncodedPtr(&statusCode, &tokenResult),
))
})

It("should create SpaceClient", func() {
NewSpaceClient(OrgName, url, Username, Password)

// Discover UAA endpoint
Expect(server.ReceivedRequests()[0].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/"))

Expect(server.ReceivedRequests()).To(HaveLen(1))
})

It("should be able to query space", func() {
spaceClient, err := NewSpaceClient(OrgName, url, Username, Password)
Expect(err).To(BeNil())

spaceClient.GetInstance(ctx, Owner)

// Discover UAA endpoint
Expect(server.ReceivedRequests()[0].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/"))
// Get new oAuth token
Expect(server.ReceivedRequests()[1].Method).To(Equal("POST"))
Expect(server.ReceivedRequests()[1].URL.Path).To(Equal(uaaURI))
// Get instance
Expect(server.ReceivedRequests()[2].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[2].URL.Path).To(Equal(serviceInstancesURI))

Expect(server.ReceivedRequests()).To(HaveLen(3))
})

It("should be able to query some space twice", func() {
spaceClient, err := NewSpaceClient(OrgName, url, Username, Password)
Expect(err).To(BeNil())

spaceClient.GetInstance(ctx, Owner)
spaceClient, err = NewSpaceClient(OrgName, url, Username, Password)
Expect(err).To(BeNil())
spaceClient.GetInstance(ctx, Owner)

// Discover UAA endpoint
Expect(server.ReceivedRequests()[0].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/"))
// Get new oAuth token
Expect(server.ReceivedRequests()[1].Method).To(Equal("POST"))
Expect(server.ReceivedRequests()[1].URL.Path).To(Equal(uaaURI))
// Get instance
Expect(server.ReceivedRequests()[2].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[2].URL.Path).To(Equal(serviceInstancesURI))

// Get instance
Expect(server.ReceivedRequests()[3].Method).To(Equal("GET"))
Expect(server.ReceivedRequests()[3].URL.Path).To(Equal(serviceInstancesURI))

Expect(server.ReceivedRequests()).To(HaveLen(4))
})
})
})