Skip to content

feat: Initial work on CyberArk Identity client #655

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

Merged
merged 6 commits into from
Jun 2, 2025
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
2 changes: 1 addition & 1 deletion LICENSES
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ github.com/asaskevich/govalidator,https://github.com/asaskevich/govalidator/blob
github.com/aymerick/douceur,https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE,MIT
github.com/beorn7/perks/quantile,https://github.com/beorn7/perks/blob/v1.0.1/LICENSE,MIT
github.com/blang/semver/v4,https://github.com/blang/semver/blob/v4.0.0/v4/LICENSE,MIT
github.com/cenkalti/backoff,https://github.com/cenkalti/backoff/blob/v2.2.1/LICENSE,MIT
github.com/cenkalti/backoff/v5,https://github.com/cenkalti/backoff/blob/v5.0.2/LICENSE,MIT
github.com/cespare/xxhash/v2,https://github.com/cespare/xxhash/blob/v2.3.0/LICENSE.txt,MIT
github.com/davecgh/go-spew/spew,https://github.com/davecgh/go-spew/blob/d8f796af33cc/LICENSE,ISC
github.com/emicklei/go-restful/v3,https://github.com/emicklei/go-restful/blob/v3.11.2/LICENSE,MIT
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.23.4

require (
github.com/Venafi/vcert/v5 v5.8.1
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cenkalti/backoff/v5 v5.0.2
github.com/d4l3k/messagediff v1.2.1
github.com/fatih/color v1.17.0
github.com/google/uuid v1.6.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
Expand Down
13 changes: 8 additions & 5 deletions pkg/agent/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"strings"
"time"

"github.com/cenkalti/backoff"
"github.com/cenkalti/backoff/v5"
"github.com/go-logr/logr"
"github.com/hashicorp/go-multierror"
"github.com/prometheus/client_golang/prometheus"
Expand Down Expand Up @@ -330,14 +330,17 @@ func gatherAndOutputData(ctx context.Context, eventf Eventf, config CombinedConf
backOff := backoff.NewExponentialBackOff()
backOff.InitialInterval = 30 * time.Second
backOff.MaxInterval = 3 * time.Minute
backOff.MaxElapsedTime = config.BackoffMaxTime
post := func() error {
return postData(klog.NewContext(ctx, log), config, preflightClient, readings)

post := func() (any, error) {
return struct{}{}, postData(klog.NewContext(ctx, log), config, preflightClient, readings)
}
err := backoff.RetryNotify(post, backOff, func(err error, t time.Duration) {

notificationFunc := backoff.Notify(func(err error, t time.Duration) {
eventf("Warning", "PushingErr", "retrying in %v after error: %s", t, err)
log.Info("Warning: PushingErr: retrying", "in", t, "reason", err)
})

_, err := backoff.Retry(ctx, post, backoff.WithBackOff(backOff), backoff.WithNotify(notificationFunc), backoff.WithMaxElapsedTime(config.BackoffMaxTime))
if err != nil {
return fmt.Errorf("Exiting due to fatal error uploading: %v", err)
}
Expand Down
151 changes: 151 additions & 0 deletions pkg/internal/cyberark/identity/advance_authentication_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package identity

import (
"context"
"fmt"
"testing"

"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
)

func Test_IdentityAdvanceAuthentication(t *testing.T) {
tests := map[string]struct {
username string
password []byte
advanceBody advanceAuthenticationRequestBody

expectedError error
}{
"success": {
username: successUser,
password: []byte(successPassword),
advanceBody: advanceAuthenticationRequestBody{
Action: ActionAnswer,
MechanismID: successMechanismID,
SessionID: successSessionID,
TenantID: "foo",
PersistantLogin: true,
},

expectedError: nil,
},
"incorrect password": {
username: successUser,
password: []byte("foo"),
advanceBody: advanceAuthenticationRequestBody{
Action: ActionAnswer,
MechanismID: successMechanismID,
SessionID: successSessionID,
TenantID: "foo",
PersistantLogin: true,
},

expectedError: fmt.Errorf(`got a failure response from request to advance authentication: message="Authentication (login or challenge) has failed. Please try again or contact your system administrator.", error="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:55555555555555555555555555555555"`),
},
"bad action": {
username: successUser,
password: []byte(successPassword),
advanceBody: advanceAuthenticationRequestBody{
Action: "foo",
MechanismID: successMechanismID,
SessionID: successSessionID,
TenantID: "foo",
PersistantLogin: true,
},

expectedError: fmt.Errorf(`got a failure response from request to advance authentication: message="Authentication (login or challenge) has failed. Please try again or contact your system administrator.", error="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:55555555555555555555555555555555"`),
},
"bad mechanism id": {
username: successUser,
password: []byte(successPassword),
advanceBody: advanceAuthenticationRequestBody{
Action: ActionAnswer,
MechanismID: "foo",
SessionID: successSessionID,
TenantID: "foo",
PersistantLogin: true,
},

expectedError: fmt.Errorf(`got a failure response from request to advance authentication: message="Authentication (login or challenge) has failed. Please try again or contact your system administrator.", error="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:55555555555555555555555555555555"`),
},
"bad session id": {
username: successUser,
password: []byte(successPassword),
advanceBody: advanceAuthenticationRequestBody{
Action: ActionAnswer,
MechanismID: successMechanismID,
SessionID: "foo",
TenantID: "foo",
PersistantLogin: true,
},

expectedError: fmt.Errorf(`got a failure response from request to advance authentication: message="Authentication (login or challenge) has failed. Please try again or contact your system administrator.", error="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:55555555555555555555555555555555"`),
},
"persistant login not set": {
username: successUser,
password: []byte(successPassword),
advanceBody: advanceAuthenticationRequestBody{
Action: ActionAnswer,
MechanismID: successMechanismID,
SessionID: successSessionID,
TenantID: "foo",
PersistantLogin: false,
},

expectedError: fmt.Errorf("got unexpected status code 403 Forbidden from request to advance authentication in CyberArk Identity API"),
},
}

for name, testSpec := range tests {
t.Run(name, func(t *testing.T) {
ctx := context.Background()

identityServer := MockIdentityServer()
defer identityServer.Close()

mockDiscoveryServer := servicediscovery.MockDiscoveryServerWithCustomAPIURL(identityServer.Server.URL)
defer mockDiscoveryServer.Close()

discoveryClient := servicediscovery.New(servicediscovery.WithCustomEndpoint(mockDiscoveryServer.Server.URL))

client, err := NewWithDiscoveryClient(ctx, discoveryClient, servicediscovery.MockDiscoverySubdomain)
if err != nil {
t.Errorf("failed to create identity client: %s", err)
return
}

err = client.doAdvanceAuthentication(ctx, testSpec.username, &testSpec.password, testSpec.advanceBody)
if testSpec.expectedError != err {
if testSpec.expectedError == nil {
t.Errorf("didn't expect an error but got %v", err)
return
}

if err == nil {
t.Errorf("expected no error but got err=%v", testSpec.expectedError)
return
}

if err.Error() != testSpec.expectedError.Error() {
t.Errorf("expected err=%v\nbut got err=%v", testSpec.expectedError, err)
return
}
}

if testSpec.expectedError != nil {
return
}

val, ok := client.tokenCache[testSpec.username]

if !ok {
t.Errorf("expected token for %s to be set to %q but wasn't found", testSpec.username, mockSuccessfulStartAuthenticationToken)
return
}

if val != mockSuccessfulStartAuthenticationToken {
t.Errorf("expected token for %s to be set to %q but was set to %q", testSpec.username, mockSuccessfulStartAuthenticationToken, val)
}
})
}
}
88 changes: 88 additions & 0 deletions pkg/internal/cyberark/identity/cmd/testidentity/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"context"
"flag"
"fmt"
"os"
"os/signal"

"k8s.io/klog/v2"

"github.com/jetstack/preflight/pkg/internal/cyberark/identity"
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
)

// This is a trivial CLI application for testing our identity client end-to-end.
// It's not intended for distribution; it simply allows us to run our client and check
// the login is successful.

const (
subdomainFlag = "subdomain"
usernameFlag = "username"
passwordEnv = "TESTIDENTITY_PASSWORD"
)

var (
subdomain string
username string
)

func run(ctx context.Context) error {
if subdomain == "" {
return fmt.Errorf("no %s flag provided", subdomainFlag)
}

if username == "" {
return fmt.Errorf("no %s flag provided", usernameFlag)
}

password := os.Getenv(passwordEnv)
if password == "" {
return fmt.Errorf("no password provided in %s", passwordEnv)
}
sdClient := servicediscovery.New(servicediscovery.WithIntegrationEndpoint())

client, err := identity.NewWithDiscoveryClient(ctx, sdClient, subdomain)
if err != nil {
return err
}

err = client.LoginUsernamePassword(ctx, username, []byte(password))
if err != nil {
return err
}

return nil
}

func main() {
defer klog.Flush()

flagSet := flag.NewFlagSet("test", flag.ExitOnError)
klog.InitFlags(flagSet)
_ = flagSet.Parse([]string{"--v", "5"})

logger := klog.Background()

ctx := klog.NewContext(context.Background(), logger)
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()

flag.StringVar(&subdomain, subdomainFlag, "cert-manager", "The subdomain to use for service discovery")
flag.StringVar(&username, usernameFlag, "",
fmt.Sprintf("Username to log in with. Password should be provided via %s envvar", passwordEnv),
)

flag.Parse()

errCode := 0

err := run(ctx)
if err != nil {
logger.Error(err, "execution failed")
errCode = 1
}

klog.FlushAndExit(klog.ExitFlushTimeout, errCode)
}
Loading