diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e2a698..fb17bc71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Add support for tracing with OpenTelemetry. This adds a new function to the authenticator, `AuthenticateWithContext`. The existing funtion, `Authenticate()` is deprecated and will be removed in a future upddate. [cyberark/conjur-authn-k8s-client#423](https://github.com/cyberark/conjur-authn-k8s-client/pull/423) +- Add support for Authn-JWT flow [cyberark/conjur-authn-k8s-client#426](https://github.com/cyberark/conjur-authn-k8s-client/pull/426) ### Changed - The project Golang version is updated from the end-of-life v1.15 to version v1.16. [cyberark/conjur-authn-k8s-client#416](https://github.com/cyberark/conjur-authn-k8s-client/pull/416) - Reduced default timeout for `waitForFile` from 1s to 50ms. [cyberark/conjur-authn-k8s-client#423](https://github.com/cyberark/conjur-authn-k8s-client/pull/423) +- Instead of getting K8S config object now you get Config Interface using NewConfigFromEnv() and ConfigFromEnv() [cyberark/conjur-authn-k8s-client#425](https://github.com/cyberark/conjur-authn-k8s-client/pull/425) +- Instead of getting K8S authenticator object now you get Authenticator Interface using NewAuthenticator() and NewAuthenticatorWithAccessToken() [cyberark/conjur-authn-k8s-client#425](https://github.com/cyberark/conjur-authn-k8s-client/pull/425) ## [0.22.0] - 2021-09-17 ### Added diff --git a/pkg/authenticator/authenticator.go b/pkg/authenticator/authenticator.go index 1df4fd62..3449ee95 100644 --- a/pkg/authenticator/authenticator.go +++ b/pkg/authenticator/authenticator.go @@ -2,9 +2,11 @@ package authenticator import ( "context" + "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token" ) type Authenticator interface { Authenticate() error AuthenticateWithContext(ctx context.Context) error + GetAccessToken() access_token.AccessToken } diff --git a/pkg/authenticator/authenticator_factory.go b/pkg/authenticator/authenticator_factory.go index e5ea2b26..23639e3c 100644 --- a/pkg/authenticator/authenticator_factory.go +++ b/pkg/authenticator/authenticator_factory.go @@ -5,6 +5,7 @@ import ( "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token" "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token/file" "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" + jwtAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/jwt" k8sAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s" "github.com/cyberark/conjur-authn-k8s-client/pkg/log" ) @@ -25,9 +26,12 @@ func NewAuthenticatorWithAccessToken(conf config.Configuration, token access_tok } func getAuthenticator(conf config.Configuration, token access_token.AccessToken) (Authenticator, error) { - if conf.GetAuthenticationType() == k8sAuthenticator.AuthnType { - k8sCfg := (conf).(*k8sAuthenticator.Config) - return k8sAuthenticator.NewWithAccessToken(*k8sCfg, token) + switch c := conf.(type) { + case *k8sAuthenticator.Config: + return k8sAuthenticator.NewWithAccessToken(*c, token) + case *jwtAuthenticator.Config: + return jwtAuthenticator.NewWithAccessToken(*c, token) + default: + return nil, fmt.Errorf(log.CAKC064) } - return nil, fmt.Errorf(log.CAKC064) } diff --git a/pkg/authenticator/k8s/tests/authenticator_test_server.go b/pkg/authenticator/common/authenticator_test_server.go similarity index 99% rename from pkg/authenticator/k8s/tests/authenticator_test_server.go rename to pkg/authenticator/common/authenticator_test_server.go index bbe69eeb..77feef98 100644 --- a/pkg/authenticator/k8s/tests/authenticator_test_server.go +++ b/pkg/authenticator/common/authenticator_test_server.go @@ -1,4 +1,4 @@ -package tests +package common import ( "crypto/rand" diff --git a/pkg/authenticator/common/common_config.go b/pkg/authenticator/common/common_config.go index b3b920fb..425bd26a 100644 --- a/pkg/authenticator/common/common_config.go +++ b/pkg/authenticator/common/common_config.go @@ -27,8 +27,12 @@ func (config *Config) LoadConfig(settings map[string]string) { case "CONJUR_ACCOUNT": config.Account = value case "CONJUR_AUTHN_LOGIN": - username, _ := NewUsername(value) - config.Username = username + if len(value) == 0 { + config.Username = nil + } else { + username, _ := NewUsername(value) + config.Username = username + } case "CONJUR_AUTHN_URL": config.URL = value case "CONJUR_SSL_CERTIFICATE": diff --git a/pkg/authenticator/common/validations.go b/pkg/authenticator/common/validations.go index 5ba7beff..6a6360aa 100644 --- a/pkg/authenticator/common/validations.go +++ b/pkg/authenticator/common/validations.go @@ -3,6 +3,8 @@ package common import ( "errors" "fmt" + "io/ioutil" + "os" "strconv" "github.com/cyberark/conjur-authn-k8s-client/pkg/log" @@ -25,6 +27,9 @@ func validInt(key, value string) error { } func validUsername(key, value string) error { + if len(value) == 0 { + return nil + } _, err := NewUsername(value) return err } @@ -53,6 +58,8 @@ func ValidateSetting(key string, value string) error { return validTimeout(key, value) case "CONJUR_VERSION": return validConjurVersion(key, value) + case "JWT_TOKEN_PATH": + return validatePath(value) default: return nil } @@ -70,3 +77,19 @@ func ReadSSLCert(settings map[string]string, readFile ReadFileFunc) ([]byte, err } return readFile(SSLCertPath) } + +func validatePath(path string) error { + // Check if file already exists + if _, err := os.Stat(path); err == nil { + return nil + } + + // Attempt to create the file and delete it right after + var emptyData []byte + if err := ioutil.WriteFile(path, emptyData, 0644); err == nil { + os.Remove(path) // And delete it + return nil + } + + return fmt.Errorf(log.CAKC065, path) +} diff --git a/pkg/authenticator/config/configuration_factory.go b/pkg/authenticator/config/configuration_factory.go index 80fd2e08..2f75892e 100644 --- a/pkg/authenticator/config/configuration_factory.go +++ b/pkg/authenticator/config/configuration_factory.go @@ -8,6 +8,7 @@ import ( "os" "strings" + jwtAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/jwt" k8sAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s" "github.com/cyberark/conjur-authn-k8s-client/pkg/log" ) @@ -24,6 +25,8 @@ func NewConfigFromEnv() (Configuration, error) { // ConfigFromEnv returns a new authenticator configuration object func ConfigFromEnv(readFileFunc common.ReadFileFunc) (Configuration, error) { + configureLogLevel(os.Getenv("DEBUG")) + authnUrl := os.Getenv(authnURLVarName) conf, error := getConfiguration(authnUrl) if error != nil { @@ -42,10 +45,14 @@ func ConfigFromEnv(readFileFunc common.ReadFileFunc) (Configuration, error) { } func getConfiguration(url string) (Configuration, error) { - if strings.Contains(url, k8sAuthenticator.AuthnType) { + switch { + case strings.Contains(url, k8sAuthenticator.AuthnType): return &k8sAuthenticator.Config{}, nil + case strings.Contains(url, jwtAuthenticator.AuthnType): + return &jwtAuthenticator.Config{}, nil + default: + return nil, fmt.Errorf(log.CAKC063, url) } - return nil, fmt.Errorf(log.CAKC063, url) } // GatherSettings retrieves authenticator client configuration settings from a slice @@ -121,3 +128,18 @@ func getConfigVariable(getters ...func(key string) string) func(string) string { return "" } } + +func configureLogLevel(level string) { + validVal := "true" + + switch level { + case validVal: + log.EnableDebugMode() + case "": + // Log level not configured + break + default: + // Log level is configured but it's invalid + log.Warn(log.CAKC034, level, validVal) + } +} diff --git a/pkg/authenticator/config/validate_test.go b/pkg/authenticator/config/validate_test.go index ae4ae0c2..c695b3cd 100644 --- a/pkg/authenticator/config/validate_test.go +++ b/pkg/authenticator/config/validate_test.go @@ -18,12 +18,12 @@ func TestValidate(t *testing.T) { assert errorAssertFunc }{ { - description: "happy path", + description: "happy path - k8s", settings: AuthnSettings{ // required variables - "CONJUR_AUTHN_URL": "filepath", + "CONJUR_AUTHN_URL": "authn-k8s", "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host", + "CONJUR_AUTHN_LOGIN": "host/myapp", "MY_POD_NAME": "testPodName", "MY_POD_NAMESPACE": "testNameSpace", // correct value types @@ -37,15 +37,49 @@ func TestValidate(t *testing.T) { }, assert: assertEmptyErrorList(), }, + { + description: "happy path - jwt", + settings: AuthnSettings{ + // required variables + "CONJUR_AUTHN_URL": "authn-jwt", + "CONJUR_ACCOUNT": "testAccount", + "JWT_TOKEN_PATH": "/tmp/token", + // correct value types + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", + "CONJUR_TOKEN_TIMEOUT": "6m0s", + "CONTAINER_MODE": "init", + // certificate provided + "CONJUR_SSL_CERTIFICATE": "samplecertificate", + }, + assert: assertEmptyErrorList(), + }, + { + description: "invalid jwt token path", + settings: AuthnSettings{ + // required variables + "CONJUR_AUTHN_URL": "authn-jwt", + "CONJUR_ACCOUNT": "testAccount", + // correct value types + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", + "CONJUR_TOKEN_TIMEOUT": "6m0s", + "CONTAINER_MODE": "init", + // certificate provided + "CONJUR_SSL_CERTIFICATE": "samplecertificate", + "JWT_TOKEN_PATH": "invalid//path", + }, + assert: assertErrorInList(fmt.Errorf(logger.CAKC065, "invalid//path")), + }, { description: "error raised for missing required setting", - settings: AuthnSettings{}, - assert: assertErrorInList(fmt.Errorf(logger.CAKC062, "CONJUR_AUTHN_URL")), + settings: AuthnSettings{ + "CONJUR_AUTHN_URL": "authn-jwt", + }, + assert: assertErrorInList(fmt.Errorf(logger.CAKC062, "CONJUR_ACCOUNT")), }, { description: "error raised for invalid username", settings: AuthnSettings{ - "CONJUR_AUTHN_URL": "filepath", + "CONJUR_AUTHN_URL": "authn-k8s", "CONJUR_ACCOUNT": "testAccount", "CONJUR_AUTHN_LOGIN": "bad-username", "MY_POD_NAME": "testPodName", @@ -56,7 +90,7 @@ func TestValidate(t *testing.T) { { description: "error raised for invalid retry count limit", settings: AuthnSettings{ - "CONJUR_AUTHN_URL": "filepath", + "CONJUR_AUTHN_URL": "authn-k8s", "CONJUR_ACCOUNT": "testAccount", "CONJUR_AUTHN_LOGIN": "host", "MY_POD_NAME": "testPodName", @@ -68,7 +102,7 @@ func TestValidate(t *testing.T) { { description: "error raised for invalid timeout", settings: AuthnSettings{ - "CONJUR_AUTHN_URL": "filepath", + "CONJUR_AUTHN_URL": "authn-k8s", "CONJUR_ACCOUNT": "testAccount", "CONJUR_AUTHN_LOGIN": "host", "MY_POD_NAME": "testPodName", @@ -81,7 +115,7 @@ func TestValidate(t *testing.T) { { description: "error raised for invalid certificate", settings: AuthnSettings{ - "CONJUR_AUTHN_URL": "filepath", + "CONJUR_AUTHN_URL": "authn-k8s", "CONJUR_ACCOUNT": "testAccount", "CONJUR_AUTHN_LOGIN": "host", "MY_POD_NAME": "testPodName", @@ -99,7 +133,8 @@ func TestValidate(t *testing.T) { for _, tc := range TestCases { t.Run(tc.description, func(t *testing.T) { // SETUP & EXERCISE - errLogs := tc.settings.validate(&k8s.Config{}, successfulMockReadFile) + configObj, _ := getConfiguration(tc.settings["CONJUR_AUTHN_URL"]) + errLogs := tc.settings.validate(configObj, successfulMockReadFile) // ASSERT tc.assert(t, errLogs) diff --git a/pkg/authenticator/jwt/authenticator.go b/pkg/authenticator/jwt/authenticator.go new file mode 100644 index 00000000..acf2b81f --- /dev/null +++ b/pkg/authenticator/jwt/authenticator.go @@ -0,0 +1,143 @@ +package jwt + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "fmt" + "go.opentelemetry.io/otel" + "net/http" + "os" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/common" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" + "github.com/cyberark/conjur-authn-k8s-client/pkg/utils" + "github.com/cyberark/secrets-provider-for-k8s/pkg/trace" +) + +// Authenticator contains the configuration and client +// for the authentication connection to Conjur +type Authenticator struct { + client *http.Client + privateKey *rsa.PrivateKey + accessToken access_token.AccessToken + Config *Config +} + +// NewWithAccessToken creates a new authenticator instance from a given access token +func NewWithAccessToken(config Config, accessToken access_token.AccessToken) (*Authenticator, error) { + signingKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, log.RecordedError(log.CAKC030, err) + } + + client, err := common.NewHTTPSClient(config.Common.SSLCertificate, nil, nil) + if err != nil { + return nil, err + } + + return &Authenticator{ + client: client, + privateKey: signingKey, + accessToken: accessToken, + Config: &config, + }, nil +} + +// GetAccessToken is getter for accessToken +func (auth *Authenticator) GetAccessToken() access_token.AccessToken { + return auth.accessToken +} + +// Authenticate sends Conjur an authenticate request and writes the response +// to the token file (after decrypting it if needed). It also manages state of +// certificates. +// @deprecated Use AuthenticateWithContext instead +func (auth *Authenticator) Authenticate() error { + return auth.AuthenticateWithContext(context.TODO()) +} + +func (auth *Authenticator) AuthenticateWithContext(ctx context.Context) error { + log.Info(log.CAKC066) + + tr := trace.NewOtelTracer(otel.Tracer("conjur-authn-k8s-client")) + spanCtx, span := tr.Start(ctx, "Authenticate") + defer span.End() + + authenticationResponse, err := auth.sendAuthenticationRequest(spanCtx, tr) + if err != nil { + span.RecordErrorAndSetStatus(err) + span.End() + return err + } + + err = auth.accessToken.Write(authenticationResponse) + if err != nil { + span.RecordErrorAndSetStatus(err) + span.End() + return err + } + + log.Info(log.CAKC035) + return nil +} + +// sendAuthenticationRequest reads the JWT token from the file system and sends +// an authentication request to the Conjur server. It also validates the response +// code before returning its body +func (auth *Authenticator) sendAuthenticationRequest(ctx context.Context, tracer trace.Tracer) ([]byte, error) { + var authenticatingIdentity string + + _, span := tracer.Start(ctx, "Send authentication request") + defer span.End() + + jwtToken, err := loadJWTToken(auth.Config.JWTTokenFilePath) + + if err != nil { + span.RecordErrorAndSetStatus(err) + return nil, err + } + + if auth.Config.Common.Username != nil { + authenticatingIdentity = auth.Config.Common.Username.FullUsername + } else { + authenticatingIdentity = "" + } + + req, err := AuthenticateRequest( + auth.Config.Common.URL, + auth.Config.Common.Account, + authenticatingIdentity, + jwtToken, + ) + + if err != nil { + span.RecordErrorAndSetStatus(err) + return nil, err + } + + resp, err := auth.client.Do(req) + + if err != nil { + span.RecordErrorAndSetStatus(err) + return nil, log.RecordedError(log.CAKC027, err) + } + + err = utils.ValidateResponse(resp) + if err != nil { + span.RecordErrorAndSetStatus(err) + return nil, err + } + + return utils.ReadResponseBody(resp) +} + +func loadJWTToken(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf(log.CAKC067, path) + } + + return string(data), nil +} diff --git a/pkg/authenticator/jwt/config.go b/pkg/authenticator/jwt/config.go new file mode 100644 index 00000000..52f57efd --- /dev/null +++ b/pkg/authenticator/jwt/config.go @@ -0,0 +1,94 @@ +package jwt + +import ( + "time" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/common" +) + +// Config defines the configuration parameters +// for the authentication requests +type Config struct { + Common common.Config + JWTTokenFilePath string +} + +// Default settings (this comment added to satisfy linter) +const ( + DefaultClientCertPath = "/etc/conjur/ssl/client.pem" + DefaultTokenFilePath = "/run/conjur/access-token" + + // DefaultTokenRefreshTimeout is the default time the system waits to reauthenticate on error + DefaultTokenRefreshTimeout = "6m0s" + + // DefaultClientCertRetryCountLimit is the amount of times we wait after successful + // login for the client certificate file to exist, where each time we wait for a second. + DefaultClientCertRetryCountLimit = "10" + + DefaultJWTTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + + AuthnType = "authn-jwt" +) + +var requiredEnvVariables = []string{ + "CONJUR_AUTHN_URL", + "CONJUR_ACCOUNT", +} + +var envVariables = []string{ + "CONJUR_ACCOUNT", + "CONJUR_AUTHN_TOKEN_FILE", + "CONJUR_AUTHN_URL", + "CONJUR_CERT_FILE", + "CONJUR_SSL_CERTIFICATE", + "CONJUR_TOKEN_TIMEOUT", + "CONTAINER_MODE", + "DEBUG", + "JWT_TOKEN_PATH", + "CONJUR_AUTHN_LOGIN", +} + +var defaultValues = map[string]string{ + "CONJUR_CLIENT_CERT_PATH": DefaultClientCertPath, + "CONJUR_AUTHN_TOKEN_FILE": DefaultTokenFilePath, + "CONJUR_TOKEN_TIMEOUT": DefaultTokenRefreshTimeout, + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": DefaultClientCertRetryCountLimit, + "JWT_TOKEN_PATH": DefaultJWTTokenPath, +} + +func (config *Config) LoadConfig(settings map[string]string) { + config.Common = common.Config{} + config.Common.LoadConfig(settings) + + if path, exists := settings["JWT_TOKEN_PATH"]; exists { + config.JWTTokenFilePath = path + } +} + +func (config *Config) GetAuthenticationType() string { + return AuthnType +} + +func (config *Config) GetEnvVariables() []string { + return envVariables +} + +func (config *Config) GetRequiredVariables() []string { + return requiredEnvVariables +} + +func (config *Config) GetDefaultValues() map[string]string { + return defaultValues +} + +func (config *Config) GetContainerMode() string { + return config.Common.ContainerMode +} + +func (config *Config) GetTokenFilePath() string { + return config.Common.TokenFilePath +} + +func (config *Config) GetTokenTimeout() time.Duration { + return config.Common.TokenRefreshTimeout +} diff --git a/pkg/authenticator/jwt/requests.go b/pkg/authenticator/jwt/requests.go new file mode 100644 index 00000000..758bfbe2 --- /dev/null +++ b/pkg/authenticator/jwt/requests.go @@ -0,0 +1,41 @@ +package jwt + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" +) + +// AuthenticateRequest sends an authenticate request +func AuthenticateRequest(authnURL string, account string, username string, jwtToken string) (*http.Request, error) { + var err error + var req *http.Request + + var authenticateURL = createUrl(authnURL, account, username) + + log.Debug(log.CAKC046, authenticateURL) + + formattedJwt := fmt.Sprintf("jwt=%s", jwtToken) + requestBody := strings.NewReader(formattedJwt) + + if req, err = http.NewRequest("POST", authenticateURL, requestBody); err != nil { + return nil, log.RecordedError(log.CAKC023, err) + } + + req.Header.Set("Content-Type", "text/plain") + req.Header.Set("Content-Length", strconv.Itoa(len(formattedJwt))) + req.Header.Set("User-Agent", "k8s") + + return req, nil +} + +func createUrl(authnURL string, account string, username string) string { + if len(username) > 0 { + return fmt.Sprintf("%s/%s/%s/authenticate", authnURL, account, url.QueryEscape(username)) + } + return fmt.Sprintf("%s/%s/authenticate", authnURL, account) +} diff --git a/pkg/authenticator/jwt/tests/authenticator_test.go b/pkg/authenticator/jwt/tests/authenticator_test.go new file mode 100644 index 00000000..fb8839fd --- /dev/null +++ b/pkg/authenticator/jwt/tests/authenticator_test.go @@ -0,0 +1,121 @@ +package tests + +import ( + "bytes" + "context" + "encoding/pem" + "github.com/stretchr/testify/assert" + "path/filepath" + "strings" + "testing" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token/memory" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/common" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/jwt" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" +) + +const tmpJwtTokenPath = "good_jwt.token" + +type assertFunc func(t *testing.T, + authn *jwt.Authenticator, + err error, +) + +func TestAuthenticator_Authenticate(t *testing.T) { + testCases := []struct { + name string + jwtTokenPath string + assert assertFunc + skipWritingCSRFile bool + wrongUrl bool + }{ + { + name: "happy path", + jwtTokenPath: tmpJwtTokenPath, + assert: func(t *testing.T, authn *jwt.Authenticator, err error) { + assert.NoError(t, err) + + // Check that the access token was set correctly + token, _ := authn.GetAccessToken().Read() + assert.Equal(t, token, []byte("some token")) + }, + }, + { + name: "wrong url given", + jwtTokenPath: tmpJwtTokenPath, + assert: func(t *testing.T, authn *jwt.Authenticator, err error) { + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "Failed to send https authenticate request or receive response")) + }, + wrongUrl: true, + }, + { + name: "token doesn't exist", + assert: func(t *testing.T, authn *jwt.Authenticator, err error) { + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "Failed reading jwt token from")) + }, + jwtTokenPath: "/tmp/nonExistingPath", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // SETUP + // Create a temporary file for storing the client cert. This will allow multiple tests to run in parallel + tmpDir := t.TempDir() + clientCertPath := filepath.Join(tmpDir, "etc:conjur:ssl:client.pem") + certLogPath := filepath.Join(tmpDir, "tmp:conjur_copy_text_output.log") + tokenPath := filepath.Join(tmpDir, "run:conjur:access-token") + + // Start up a test server to mock the Conjur server's auth endpoints + ts := common.NewTestAuthServer(clientCertPath, certLogPath, "some token", tc.skipWritingCSRFile) + + defer ts.Server.Close() + + // Create an authenticator with dummy config + at, _ := memory.NewAccessToken() + + sslcert := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: ts.Server.Certificate().Raw, + }) + + cfg := jwt.Config{ + JWTTokenFilePath: tc.jwtTokenPath, + Common: common.Config{ + SSLCertificate: sslcert, + TokenFilePath: tokenPath, + TokenRefreshTimeout: 0, + URL: ts.Server.URL, + Username: nil, + Account: "account", + ClientCertPath: clientCertPath, + ClientCertRetryCountLimit: 0, + ContainerMode: "doesntmatter", + }, + } + + if tc.wrongUrl { + cfg.Common.URL = "http://wrong-url" + } + + // EXERCISE + authn, err := jwt.NewWithAccessToken(cfg, at) + if !assert.NoError(t, err) { + return + } + + // Intercept the logs to check for the cert placement error + var logTxt bytes.Buffer + log.ErrorLogger.SetOutput(&logTxt) + + // Call the main method of the authenticator. This is where most of the internal implementation happens + err = authn.AuthenticateWithContext(context.Background()) + + // ASSERT + tc.assert(t, authn, err) + }) + } +} diff --git a/pkg/authenticator/jwt/tests/config_test.go b/pkg/authenticator/jwt/tests/config_test.go new file mode 100644 index 00000000..53469169 --- /dev/null +++ b/pkg/authenticator/jwt/tests/config_test.go @@ -0,0 +1,148 @@ +package tests + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/jwt" +) + +var environmentValues = map[string]string{ + "CONJUR_AUTHN_URL": "authn-jwt", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_CERT_FILE": "testSSLCertFile.txt", + "CONJUR_SSL_CERTIFICATE": "testSSLCert", + "JWT_TOKEN_PATH": "good_jwt.token", +} + +var annotationValues = map[string]string{ + "conjur.org/debug-logging": "true", + "conjur.org/container-mode": "init", +} + +var envToAnnot = map[string]string{ + "DEBUG": "conjur.org/debug-logging", + "CONTAINER_MODE": "conjur.org/container-mode", +} + +func setEnv(env map[string]string) { + for key, value := range env { + os.Setenv(key, value) + } +} + +func unsetEnv(env map[string]string) { + for key := range env { + os.Setenv(key, "") + } +} + +func TestGatherSettings(t *testing.T) { + TestCases := []struct { + description string + annotFunc func(string) string + expected config.AuthnSettings + }{ + { + description: "functions are ordered by priority: first function overrides second, which overrides third", + annotFunc: fromAnnotations, + expected: config.AuthnSettings{ + "JWT_TOKEN_PATH": "good_jwt.token", + "CONJUR_AUTHN_LOGIN": "", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_AUTHN_URL": "authn-jwt", + "CONJUR_CERT_FILE": "testSSLCertFile.txt", + "CONJUR_SSL_CERTIFICATE": "testSSLCert", + "CONTAINER_MODE": "init", // provided by annotation + "DEBUG": "true", // provided by annotation + "CONJUR_AUTHN_TOKEN_FILE": jwt.DefaultTokenFilePath, + "CONJUR_TOKEN_TIMEOUT": jwt.DefaultTokenRefreshTimeout, + }, + }, + { + description: "if the first getter function returns empty strings, fallback to the next functions, and eventually an empty string", + annotFunc: emptyAnnotations, + expected: config.AuthnSettings{ + "JWT_TOKEN_PATH": "good_jwt.token", + "CONJUR_AUTHN_LOGIN": "", + "CONJUR_AUTHN_URL": "authn-jwt", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_CERT_FILE": "testSSLCertFile.txt", + "CONJUR_SSL_CERTIFICATE": "testSSLCert", + "DEBUG": "", + "CONTAINER_MODE": "", + "CONJUR_AUTHN_TOKEN_FILE": jwt.DefaultTokenFilePath, + "CONJUR_TOKEN_TIMEOUT": jwt.DefaultTokenRefreshTimeout, + }, + }, + } + + for _, tc := range TestCases { + t.Run(tc.description, func(t *testing.T) { + resultMap := config.GatherSettings(&jwt.Config{}, tc.annotFunc, fromEnv) + assert.Equal(t, tc.expected, resultMap) + }) + } +} + +func TestFromEnv(t *testing.T) { + TestCases := []struct { + description string + env map[string]string + expectErr bool + }{ + { + description: "happy path", + env: map[string]string{ + "JWT_TOKEN_PATH": "good_jwt.token", + "CONJUR_AUTHN_URL": "authn-jwt", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_SSL_CERTIFICATE": "samplecert", + }, + expectErr: false, + }, + { + description: "bad settings return nil Config and error message", + env: map[string]string{}, + expectErr: true, + }, + } + + for _, tc := range TestCases { + t.Run(tc.description, func(t *testing.T) { + // SETUP & EXERCISE + setEnv(tc.env) + config, err := config.ConfigFromEnv(successfulMockReadFile) + + // ASSERT + if tc.expectErr { + assert.Nil(t, config) + assert.NotNil(t, err) + } else { + assert.NotNil(t, config) + assert.Nil(t, err) + } + unsetEnv(tc.env) + }) + } +} + +func successfulMockReadFile(filename string) ([]byte, error) { + return []byte{}, nil +} + +func fromEnv(key string) string { + return environmentValues[key] +} + +func fromAnnotations(key string) string { + annot := envToAnnot[key] + return annotationValues[annot] +} + +func emptyAnnotations(key string) string { + return "" +} diff --git a/pkg/authenticator/jwt/tests/good_jwt.token b/pkg/authenticator/jwt/tests/good_jwt.token new file mode 100644 index 00000000..157b7b5d --- /dev/null +++ b/pkg/authenticator/jwt/tests/good_jwt.token @@ -0,0 +1 @@ +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c \ No newline at end of file diff --git a/pkg/authenticator/k8s/authenticator.go b/pkg/authenticator/k8s/authenticator.go index 14343ca6..2547f838 100644 --- a/pkg/authenticator/k8s/authenticator.go +++ b/pkg/authenticator/k8s/authenticator.go @@ -35,8 +35,8 @@ var bufferTime = 30 * time.Second type Authenticator struct { client *http.Client privateKey *rsa.PrivateKey - AccessToken access_token.AccessToken - Config *Config + accessToken access_token.AccessToken + config *Config PublicCert *x509.Certificate } @@ -62,11 +62,16 @@ func NewWithAccessToken(config Config, accessToken access_token.AccessToken) (*A return &Authenticator{ client: client, privateKey: signingKey, - AccessToken: accessToken, - Config: &config, + accessToken: accessToken, + config: &config, }, nil } +// GetAccessToken is getter for accessToken +func (auth *Authenticator) GetAccessToken() access_token.AccessToken { + return auth.accessToken +} + // Authenticate sends Conjur an authenticate request and writes the response // to the token file (after decrypting it if needed). It also manages state of // certificates. @@ -76,7 +81,7 @@ func (auth *Authenticator) Authenticate() error { } func (auth *Authenticator) AuthenticateWithContext(ctx context.Context) error { - log.Info(log.CAKC040, auth.Config.Common.Username) + log.Info(log.CAKC040, auth.config.Common.Username) tr := trace.NewOtelTracer(otel.Tracer("conjur-authn-k8s-client")) spanCtx, span := tr.Start(ctx, "Authenticate") @@ -103,7 +108,7 @@ func (auth *Authenticator) AuthenticateWithContext(ctx context.Context) error { return err } - err = auth.AccessToken.Write(parsedResponse) + err = auth.accessToken.Write(parsedResponse) if err != nil { return err } @@ -114,7 +119,7 @@ func (auth *Authenticator) AuthenticateWithContext(ctx context.Context) error { // generateCSR prepares the CSR func (auth *Authenticator) generateCSR(commonName string) ([]byte, error) { - sanURIString, err := generateSANURI(auth.Config.PodNamespace, auth.Config.PodName) + sanURIString, err := generateSANURI(auth.config.PodNamespace, auth.config.PodName) sanURI, err := url.Parse(sanURIString) if err != nil { return nil, err @@ -150,17 +155,17 @@ func (auth *Authenticator) generateCSR(commonName string) ([]byte, error) { // successfully retrieved func (auth *Authenticator) login(ctx context.Context, tracer trace.Tracer) error { - log.Debug(log.CAKC041, auth.Config.Common.Username) + log.Debug(log.CAKC041, auth.config.Common.Username) _, span := tracer.Start(ctx, "Generate CSR") - csrRawBytes, err := auth.generateCSR(auth.Config.Common.Username.Suffix) + csrRawBytes, err := auth.generateCSR(auth.config.Common.Username.Suffix) csrBytes := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE REQUEST", Bytes: csrRawBytes, }) span.End() - req, err := LoginRequest(auth.Config.Common.URL, auth.Config.ConjurVersion, csrBytes, auth.Config.Common.Username.Prefix) + req, err := LoginRequest(auth.config.Common.URL, auth.config.ConjurVersion, csrBytes, auth.config.Common.Username.Prefix) if err != nil { return err } @@ -183,8 +188,8 @@ func (auth *Authenticator) login(ctx context.Context, tracer trace.Tracer) error // Ensure client certificate exists before attempting to read it, with a tolerance // for small delays err = utils.WaitForFile( - auth.Config.Common.ClientCertPath, - auth.Config.Common.ClientCertRetryCountLimit, + auth.config.Common.ClientCertPath, + auth.config.Common.ClientCertRetryCountLimit, ) if err != nil { // The response code was changed from 200 to 202 in the same Conjur version @@ -192,7 +197,7 @@ func (auth *Authenticator) login(ctx context.Context, tracer trace.Tracer) error // the response code is 202 will verify that we look for the log file only // if we expect it to be there if resp.StatusCode == 202 { - injectClientCertError := consumeInjectClientCertError(auth.Config.InjectCertLogPath) + injectClientCertError := consumeInjectClientCertError(auth.config.InjectCertLogPath) if injectClientCertError != "" { log.Error(log.CAKC055, injectClientCertError) } @@ -205,34 +210,34 @@ func (auth *Authenticator) login(ctx context.Context, tracer trace.Tracer) error _, span = tracer.Start(ctx, "Load cert file") // load client cert - certPEMBlock, err := ioutil.ReadFile(auth.Config.Common.ClientCertPath) + certPEMBlock, err := ioutil.ReadFile(auth.config.Common.ClientCertPath) if err != nil { span.RecordErrorAndSetStatus(err) span.End() if os.IsNotExist(err) { - return log.RecordedError(log.CAKC011, auth.Config.Common.ClientCertPath) + return log.RecordedError(log.CAKC011, auth.config.Common.ClientCertPath) } return log.RecordedError(log.CAKC012, err) } - log.Debug(log.CAKC049, auth.Config.Common.ClientCertPath) + log.Debug(log.CAKC049, auth.config.Common.ClientCertPath) certDERBlock, certPEMBlock := pem.Decode(certPEMBlock) cert, err := x509.ParseCertificate(certDERBlock.Bytes) if err != nil { - return log.RecordedError(log.CAKC013, auth.Config.Common.ClientCertPath, err) + return log.RecordedError(log.CAKC013, auth.config.Common.ClientCertPath, err) span.RecordErrorAndSetStatus(err) span.End() - return log.RecordedError(log.CAKC013, auth.Config.Common.ClientCertPath, err) + return log.RecordedError(log.CAKC013, auth.config.Common.ClientCertPath, err) } auth.PublicCert = cert span.End() // clean up the client cert so it's only available in memory - os.Remove(auth.Config.Common.ClientCertPath) + os.Remove(auth.config.Common.ClientCertPath) log.Debug(log.CAKC050) return nil @@ -293,17 +298,17 @@ func (auth *Authenticator) sendAuthenticationRequest(ctx context.Context, tracer certPEMBlock := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: auth.PublicCert.Raw}) - client, err := common.NewHTTPSClient(auth.Config.Common.SSLCertificate, certPEMBlock, keyPEMBlock) + client, err := common.NewHTTPSClient(auth.config.Common.SSLCertificate, certPEMBlock, keyPEMBlock) if err != nil { span.RecordErrorAndSetStatus(err) return nil, err } req, err := AuthenticateRequest( - auth.Config.Common.URL, - auth.Config.ConjurVersion, - auth.Config.Common.Account, - auth.Config.Common.Username.FullUsername, + auth.config.Common.URL, + auth.config.ConjurVersion, + auth.config.Common.Account, + auth.config.Common.Username.FullUsername, ) if err != nil { span.RecordErrorAndSetStatus(err) @@ -335,13 +340,13 @@ func (auth *Authenticator) parseAuthenticationResponse(ctx context.Context, trac var err error // Token is only encrypted in Conjur v4 - if auth.Config.ConjurVersion == "4" { + if auth.config.ConjurVersion == "4" { content, err = decodeFromPEM(response, auth.PublicCert, auth.privateKey) if err != nil { span.RecordErrorAndSetStatus(err) return nil, log.RecordedError(log.CAKC020) } - } else if auth.Config.ConjurVersion == "5" { + } else if auth.config.ConjurVersion == "5" { content = response } @@ -378,7 +383,7 @@ func marshalSANs(dnsNames, emailAddresses []string, ipAddresses []net.IP, uris [ return asn1.Marshal(rawValues) } -func decodeFromPEM(PEMBlock []byte, publicCert *x509.Certificate, privateKey crypto.PrivateKey) ([]byte, error) { +func decodeFromPEM(PEMBlock []byte, PublicCert *x509.Certificate, privateKey crypto.PrivateKey) ([]byte, error) { var decodedPEM []byte tokenDerBlock, _ := pem.Decode(PEMBlock) @@ -387,7 +392,7 @@ func decodeFromPEM(PEMBlock []byte, publicCert *x509.Certificate, privateKey cry return nil, log.RecordedError(log.CAKC026, err) } - decodedPEM, err = p7.Decrypt(publicCert, privateKey) + decodedPEM, err = p7.Decrypt(PublicCert, privateKey) if err != nil { return nil, log.RecordedError(log.CAKC025, err) } diff --git a/pkg/authenticator/k8s/config.go b/pkg/authenticator/k8s/config.go index b8535662..6cad70d7 100644 --- a/pkg/authenticator/k8s/config.go +++ b/pkg/authenticator/k8s/config.go @@ -76,19 +76,7 @@ func durationFromString(key, value string) (time.Duration, error) { return duration, nil } -func configureLogLevel(level string) { - validVal := "true" - if level == validVal { - log.EnableDebugMode() - } else if level != "" { - // In case "DEBUG" is configured with incorrect value - log.Warn(log.CAKC034, level, validVal) - } -} - func (config *Config) LoadConfig(settings map[string]string) { - configureLogLevel(settings["DEBUG"]) - config.Common = common.Config{} config.Common.LoadConfig(settings) diff --git a/pkg/authenticator/k8s/tests/authenticator_test.go b/pkg/authenticator/k8s/tests/authenticator_test.go index d2bb1e6a..d582f77d 100644 --- a/pkg/authenticator/k8s/tests/authenticator_test.go +++ b/pkg/authenticator/k8s/tests/authenticator_test.go @@ -61,7 +61,7 @@ func TestAuthenticator_Authenticate(t *testing.T) { ) // Check that the access token was set correctly - token, _ := authn.AccessToken.Read() + token, _ := authn.GetAccessToken().Read() assert.Equal(t, token, []byte("some token")) }, }, @@ -121,7 +121,7 @@ func TestAuthenticator_Authenticate(t *testing.T) { var loginCsrErr error // Start up a test server to mock the Conjur server's auth endpoints - ts := NewTestAuthServer(clientCertPath, certLogPath, "some token", tc.skipWritingCSRFile) + ts := common.NewTestAuthServer(clientCertPath, certLogPath, "some token", tc.skipWritingCSRFile) ts.HandleLogin = func(csr *x509.CertificateRequest, err error) { loginCsr = csr loginCsrErr = err diff --git a/pkg/authenticator/k8s/tests/config_test.go b/pkg/authenticator/k8s/tests/config_test.go index b092a072..00728f01 100644 --- a/pkg/authenticator/k8s/tests/config_test.go +++ b/pkg/authenticator/k8s/tests/config_test.go @@ -1,13 +1,13 @@ package tests import ( - "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" - "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s" - "os" "testing" "github.com/stretchr/testify/assert" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s" ) var environmentValues = map[string]string{ @@ -33,14 +33,6 @@ var envToAnnot = map[string]string{ "CONTAINER_MODE": "conjur.org/container-mode", } -func assertGoodConfig(expected *k8s.Config) func(*testing.T, *k8s.Config) { - return func(t *testing.T, result *k8s.Config) { - assert.Equal(t, expected, result) - } -} - -type errorAssertFunc func(*testing.T, []error) - func setEnv(env map[string]string) { for key, value := range env { os.Setenv(key, value) diff --git a/pkg/log/log_messages.go b/pkg/log/log_messages.go index 0ca30c30..b2b4a666 100644 --- a/pkg/log/log_messages.go +++ b/pkg/log/log_messages.go @@ -77,3 +77,6 @@ const CAKC061 string = "CAKC061 Failed to validate setting for Authenticator con const CAKC062 string = "CAKC062 Required Authenticator setting %s not provided" const CAKC063 string = "CAKC063 Unable to find configuration for URL: %s" const CAKC064 string = "CAKC064 Unable to find Authenticator for this configuration" +const CAKC065 string = "CAKC065 File path is invalid %s" +const CAKC066 string = "CAKC066 Performing authn-jwt" +const CAKC067 string = "CAKC067 Failed reading jwt token from %s"