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
73 changes: 73 additions & 0 deletions pkg/keycloak/keycloak.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,79 @@ func NewHelper(opts ...Option) (*Helper, error) {
return h, nil
}

/*
* HelperWithoutCredentials is a keycloak client for call patterns like
* CheckLoginUser that take login credentials as call-time parameters rather
* than pre-configured Options. Unlike Helper, it has no LoginAdmin or
* LoginServiceAccount methods, so a caller can't accidentally try to log in
* with Options-level credentials that were never required to construct it --
* that misuse is a compile error here instead of a runtime one.
*/
type HelperWithoutCredentials struct {
Client
Options
}

func NewHelperWithoutCredentials(opts ...Option) (*HelperWithoutCredentials, error) {
initedOpts := initOptions(opts)
h := &HelperWithoutCredentials{Options: *initedOpts}

if h.Options.Scheme == "" {
return nil, fmt.Errorf("keycloak scheme is empty")
}

if h.Options.Ip == "" {
return nil, fmt.Errorf("keycloak ip is empty")
}

if h.Options.Port == 0 {
return nil, fmt.Errorf("keycloak port is empty")
}

if h.Options.Path == "" {
return nil, fmt.Errorf("keycloak path is empty")
}

if h.Options.Realm == "" {
return nil, fmt.Errorf("keycloak realm is empty")
}

h.Client = gocloak.NewClient(h.genKeycloakUrl())
return h, nil
}

func (h *HelperWithoutCredentials) genKeycloakUrl() string {
u := url.URL{}
u.Scheme = h.Options.Scheme
u.Host = fmt.Sprintf("%s:%d", h.Options.Ip, h.Options.Port)
u.Path = h.Options.Path
return u.String()
}

func (h *HelperWithoutCredentials) CheckLoginUser(username, password string, client ClientCredentials) (*gocloak.JWT, error) {
if h.Options.TlsInsecureSkipVerify {
h.Client.RestyClient().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}

ctx, cancel := context.WithTimeout(wait.CtxSeconds(10))
defer cancel()
token, err := h.Client.Login(
ctx,
client.ID,
client.Secret,
h.Options.Realm,
username,
password,
)
if err != nil {
// %w (not LoginAdmin's %s) so callers can errors.As into gocloak's *APIError
// to tell an invalid-credentials response apart from an unexpected failure.
return nil, fmt.Errorf("keycloak login failed: %w", err)
}

return token, nil
}

func NewGlobalHelper(opts ...Option) error {
var err error
once.Do(func() {
Expand Down
100 changes: 100 additions & 0 deletions pkg/keycloak/keycloak_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,106 @@ func TestSetKeycloakClient(t *testing.T) {
}
}

func TestNewHelperWithoutCredentials(t *testing.T) {
tests := []struct {
name string
opts []Option
expectedError string
}{
{
name: "Should return an error if scheme is empty",
opts: []Option{Ip("keycloak"), Port(80), Path("auth"), Realm("master")},
expectedError: "keycloak scheme is empty",
},
{
name: "Should return an error if ip is empty",
opts: []Option{Scheme("http"), Port(80), Path("auth"), Realm("master")},
expectedError: "keycloak ip is empty",
},
{
name: "Should return an error if port is empty",
opts: []Option{Scheme("http"), Ip("keycloak"), Path("auth"), Realm("master")},
expectedError: "keycloak port is empty",
},
{
name: "Should return an error if path is empty",
opts: []Option{Scheme("http"), Ip("keycloak"), Port(80), Realm("master")},
expectedError: "keycloak path is empty",
},
{
name: "Should return an error if realm is empty",
opts: []Option{Scheme("http"), Ip("keycloak"), Port(80), Path("auth")},
expectedError: "keycloak realm is empty",
},
{
name: "Should succeed with no credentials set",
opts: []Option{Scheme("http"), Ip("keycloak"), Port(80), Path("auth"), Realm("master")},
},
{
name: "Should succeed even when credentials happen to be set",
opts: []Option{Scheme("http"), Ip("keycloak"), Port(80), Path("auth"), Realm("master"), Username("admin"), Password("admin")},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h, err := NewHelperWithoutCredentials(tc.opts...)

if tc.expectedError == "" {
require.NoError(t, err)
require.NotNil(t, h.Client)
} else {
require.EqualError(t, err, tc.expectedError)
require.Nil(t, h)
}
})
}
}

func TestCheckLoginUserWithHelperBuiltWithoutCredentials(t *testing.T) {
tests := []struct {
name string
username string
password string
client ClientCredentials
token *gocloak.JWT
expected *gocloak.JWT
}{
{
name: "Should let CheckLoginUser use call-time credentials when NewHelperWithoutCredentials was built from network-location options alone",
username: "user",
password: "pass",
client: DefaultAdmin,
token: &gocloak.JWT{AccessToken: "access-token"},
expected: &gocloak.JWT{AccessToken: "access-token"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h, err := NewHelperWithoutCredentials(
Scheme("http"),
Ip("keycloak"),
Port(80),
Path("auth"),
Realm("master"),
)
require.NoError(t, err)
require.NotNil(t, h.Client)

client := NewMockClient(t)
client.On("Login", mock.Anything, tc.client.ID, tc.client.Secret, "master", tc.username, tc.password).
Return(tc.token, nil)
h.Client = client

got, err := h.CheckLoginUser(tc.username, tc.password, tc.client)

require.NoError(t, err)
require.Equal(t, tc.expected, got)
})
}
}

func TestHelperExecuteActionsEmail(t *testing.T) {
errBoom := errors.New("boom")

Expand Down
Loading