├── .dockerignore ├── .gitignore ├── secrets.yml ├── conjurapi ├── client_v2.go ├── authn │ ├── token_authenticator.go │ ├── wait_for_file.go │ ├── token_authenticator_test.go │ ├── api_key_authenticator.go │ ├── oidc_authenticator_test.go │ ├── oidc_authenticator.go │ ├── wait_for_file_test.go │ ├── token_file_authenticator.go │ ├── api_key_authenticator_test.go │ ├── jwt_authenticator.go │ ├── iam_authenticator_test.go │ ├── jwt_authenticator_test.go │ ├── auth_token.go │ ├── gcp_authenticator.go │ ├── azure_authenticator.go │ ├── iam_authenticator.go │ ├── token_file_authenticator_test.go │ └── auth_token_test.go ├── env_test.go ├── logging │ ├── logging.go │ └── logging_test.go ├── version_test.go ├── version.go ├── response │ ├── error.go │ ├── response.go │ └── error_test.go ├── router_url.go ├── router_url_test.go ├── storage.go ├── environment.go ├── storage │ ├── keyring_storage_provider.go │ └── netrc_storage_provider.go ├── role.go ├── authn_iam_test.go ├── host_factory.go ├── requests_v2.go ├── group_membership_v2_test.go ├── workload_v2.go ├── group_membership_v2.go ├── role_test.go ├── storage_test.go ├── resource_json_test.go ├── requests_test.go ├── environment_test.go ├── policy.go ├── issuer_v2.go ├── authn_gcp_test.go ├── variable.go ├── info.go ├── host_factory_test.go ├── secret_static_v2.go ├── issuer.go ├── resource.go ├── authenticators.go └── secret_static_v2_test.go ├── SECURITY.md ├── bin ├── dev.sh ├── package.sh ├── start-conjur.sh ├── get_gcp_token.sh ├── utils.sh └── test.sh ├── Dockerfile ├── .github └── CODEOWNERS ├── kics.config ├── go.mod ├── docker-compose.yml ├── CONTRIBUTING.md └── go.sum /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | output/ 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | /output/ 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /secrets.yml: -------------------------------------------------------------------------------- 1 | AZURE_SUBSCRIPTION_ID: !var ci/azure/subscription-id 2 | AZURE_RESOURCE_GROUP: !var ci/azure/authn-test/resource-group 3 | USER_ASSIGNED_IDENTITY: !var ci/azure/authn-test/user-assigned-id 4 | USER_ASSIGNED_IDENTITY_CLIENT_ID: !var ci/azure/authn-test/user-assigned-id-client-id 5 | -------------------------------------------------------------------------------- /conjurapi/client_v2.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | const MinVersion = "1.23.0" 4 | const NotSupportedInConjurCloud = "%s is not supported in Secrets Manager SaaS" 5 | const NotSupportedInConjurEnterprise = "%s is not supported in Conjur Enterprise/OSS" 6 | const NotSupportedInOldVersions = "%s is not supported in Conjur versions older than %s" 7 | 8 | type ClientV2 struct { 9 | *Client 10 | } 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policies and Procedures 2 | 3 | ## Reporting a Bug 4 | CyberArk takes product security very seriously. If you believe you have found a vulnerability in one of our products, we ask that you follow responsible disclosure guidelines and contact product_security@cyberark.com and work with us toward a quick resolution to protect our customers. 5 | 6 | Refer to [CyberArk's Security Vulnerability Policy](https://www.cyberark.com/cyberark-security-vulinerability-policy.pdf) for more details. -------------------------------------------------------------------------------- /conjurapi/authn/token_authenticator.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | // TokenAuthenticator handles authentication to Conjur where a Conjur access token is provided directly. 4 | type TokenAuthenticator struct { 5 | Token string `env:"CONJUR_AUTHN_TOKEN"` 6 | } 7 | 8 | // RefreshToken returns the provided Conjur access token. 9 | func (a *TokenAuthenticator) RefreshToken() ([]byte, error) { 10 | return []byte(a.Token), nil 11 | } 12 | 13 | func (a *TokenAuthenticator) NeedsTokenRefresh() bool { 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /bin/dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash -ex 2 | 3 | cd "$(dirname "$0")" 4 | . ./utils.sh 5 | 6 | source ./start-conjur.sh 7 | 8 | docker compose build dev 9 | docker compose up --no-deps -d dev 10 | 11 | # When we start the dev container, it mounts the top-level directory in 12 | # the container. This excludes the vendored dependencies that got 13 | # installed during the build, so reinstall them. 14 | exec_on dev go mod download 15 | 16 | # Start interactive container 17 | docker exec -it \ 18 | -e CONJUR_AUTHN_API_KEY \ 19 | "$(docker compose ps -q dev)" /bin/bash 20 | -------------------------------------------------------------------------------- /bin/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | cd "$(dirname "$0")" 4 | 5 | echo "==> Packaging..." 6 | build_dir="../output/dist" 7 | rm -rf "$build_dir" 8 | mkdir -p "$build_dir" 9 | 10 | tar --exclude='../.git' --exclude='../output' -cvzf "$build_dir/conjur-api-go.tar.gz" . 11 | 12 | # # Make the checksums 13 | echo "==> Checksumming..." 14 | pushd "$build_dir" 15 | if which sha256sum; then 16 | sha256sum * > SHA256SUMS.txt 17 | elif which shasum; then 18 | shasum -a256 * > SHA256SUMS.txt 19 | else 20 | echo "couldn't find sha256sum or shasum" 21 | exit 1 22 | fi 23 | popd 24 | -------------------------------------------------------------------------------- /conjurapi/env_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | func splitEq(s string) (string, string) { 9 | a := strings.SplitN(s, "=", 2) 10 | return a[0], a[1] 11 | } 12 | 13 | type envSnapshot struct { 14 | env []string 15 | } 16 | 17 | func ClearEnv() *envSnapshot { 18 | e := os.Environ() 19 | 20 | for _, s := range e { 21 | k, _ := splitEq(s) 22 | os.Setenv(k, "") 23 | } 24 | return &envSnapshot{env: e} 25 | } 26 | 27 | func (e *envSnapshot) RestoreEnv() { 28 | ClearEnv() 29 | for _, s := range e.env { 30 | k, v := splitEq(s) 31 | os.Setenv(k, v) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /conjurapi/authn/wait_for_file.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | func waitForTextFile(fileName string, timeout <-chan time.Time) ([]byte, error) { 10 | var ( 11 | fileBytes []byte 12 | err error 13 | ) 14 | 15 | waiting_loop: 16 | for { 17 | select { 18 | case <-timeout: 19 | err = fmt.Errorf("Operation waitForTextFile timed out.") 20 | break waiting_loop 21 | default: 22 | if _, err := os.Stat(fileName); os.IsNotExist(err) { 23 | time.Sleep(100 * time.Millisecond) 24 | } else { 25 | fileBytes, err = os.ReadFile(fileName) 26 | break waiting_loop 27 | } 28 | } 29 | } 30 | 31 | return fileBytes, err 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG FROM_IMAGE="golang:1.25" 2 | FROM ${FROM_IMAGE} 3 | LABEL maintainer="CyberArk Software Ltd." 4 | 5 | CMD ["/bin/bash"] 6 | EXPOSE 8080 7 | 8 | RUN apt-get update -y && \ 9 | apt-get install -y --no-install-recommends \ 10 | bash \ 11 | gcc \ 12 | git \ 13 | jq \ 14 | less \ 15 | libc-dev 16 | 17 | RUN go install github.com/jstemmer/go-junit-report@latest && \ 18 | go install github.com/afunix/gocov/gocov@latest && \ 19 | go install github.com/AlekSi/gocov-xml@latest && \ 20 | go install github.com/wadey/gocovmerge@latest 21 | 22 | WORKDIR /conjur-api-go 23 | 24 | COPY go.mod go.sum ./ 25 | RUN go mod download 26 | 27 | COPY . . 28 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cyberark/community-and-integrations-team @conjurinc/community-and-integrations-team @conjurdemos/community-and-integrations-team @conjur-enterprise/community-and-integrations 2 | 3 | # Changes to .trivyignore require Security Architect approval 4 | .trivyignore @cyberark/security-architects @conjurinc/security-architects @conjurdemos/security-architects @conjur-enterprise/conjur-security 5 | 6 | # Changes to .codeclimate.yml require Quality Architect approval 7 | .codeclimate.yml @cyberark/quality-architects @conjurinc/quality-architects @conjurdemos/quality-architects @conjur-enterprise/conjur-quality 8 | 9 | # Changes to SECURITY.md require Security Architect approval 10 | SECURITY.md @cyberark/security-architects @conjurinc/security-architects @conjurdemos/security-architects @conjur-enterprise/conjur-security 11 | -------------------------------------------------------------------------------- /kics.config: -------------------------------------------------------------------------------- 1 | exclude-queries: 2 | - 965a08d7-ef86-4f14-8792-4a3b2098937e # Apt Get Install Pin Version Not Defined 3 | - fd54f200-402c-4333-a5a4-36ef6709af2f # Missing User Instruction 4 | - ce76b7d0-9e77-464d-b86f-c5c48e03e22d # Container Capabilities Unrestricted 5 | - 8c978947-0ff6-485c-b0c2-0bfca6026466 # Shared Volumes Between Containers 6 | - 610e266e-6c12-4bca-9925-1ed0cd29742b # Security Opt Not Set 7 | - b03a748a-542d-44f4-bb86-9199ab4fd2d5 # Healthcheck Instruction Missing 8 | - 698ed579-b239-4f8f-a388-baa4bcb13ef8 # Healthcheck Not Set 9 | - 451d79dc-0588-476a-ad03-3c7f0320abb3 # Container Traffic Not Bound To Host Interface 10 | - df746b39-6564-4fed-bf85-e9c44382303c # Apt Get Install Lists Were Not Deleted 11 | - 4f31dd9f-2cc3-4751-9b53-67e4af83dac0 # Host Namespace is Shared 12 | - ce14a68b-1668-41a0-ab7d-facd9f784742 # Networks Not Set 13 | -------------------------------------------------------------------------------- /conjurapi/authn/token_authenticator_test.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTokenAuthenticator_RefreshToken(t *testing.T) { 10 | // Test that the RefreshToken method returns the token 11 | t.Run("Returns token", func(t *testing.T) { 12 | authenticator := TokenAuthenticator{ 13 | Token: "token", 14 | } 15 | token, err := authenticator.RefreshToken() 16 | assert.NoError(t, err) 17 | assert.Equal(t, []byte("token"), token) 18 | }) 19 | } 20 | 21 | func TestTokenAuthenticator_NeedsTokenRefresh(t *testing.T) { 22 | t.Run("Returns false", func(t *testing.T) { 23 | // Test that the NeedsTokenRefresh method always returns false 24 | authenticator := TokenAuthenticator{ 25 | Token: "token", 26 | } 27 | 28 | assert.False(t, authenticator.NeedsTokenRefresh()) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /conjurapi/authn/api_key_authenticator.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | type APIKeyAuthenticator struct { 4 | // Authenticate is a function that takes a LoginPair and returns a JWT token or an error. 5 | // It will usually be set to Client.Authenticate. 6 | Authenticate func(loginPair LoginPair) ([]byte, error) 7 | // LoginPair holds the login and API key for authentication. 8 | LoginPair 9 | } 10 | 11 | type LoginPair struct { 12 | Login string 13 | APIKey string 14 | } 15 | 16 | func (a *APIKeyAuthenticator) RefreshToken() ([]byte, error) { 17 | // Call the Authenticate function with the stored LoginPair to get a new Conjur access token. 18 | return a.Authenticate(a.LoginPair) 19 | } 20 | 21 | func (a *APIKeyAuthenticator) NeedsTokenRefresh() bool { 22 | // API Key authentication does not require token refresh logic. 23 | // Expiration of the access token is handled by the Client (see NeedsTokenRefresh in authn.go). 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /bin/start-conjur.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | . ./utils.sh 4 | 5 | trap teardown ERR 6 | 7 | announce "Compose Project Name: $COMPOSE_PROJECT_NAME" 8 | 9 | main() { 10 | announce "Pulling images..." 11 | docker compose pull "conjur" "postgres" 12 | echo "Done!" 13 | 14 | announce "Building images..." 15 | docker compose build "conjur" "postgres" 16 | echo "Done!" 17 | 18 | announce "Starting Conjur environment..." 19 | export CONJUR_DATA_KEY="$(docker compose run --rm -T --no-deps conjur data-key generate)" 20 | docker compose up --no-deps -d "conjur" "postgres" 21 | echo "Done!" 22 | 23 | announce "Waiting for conjur to start..." 24 | exec_on conjur conjurctl wait 25 | 26 | echo "Done!" 27 | 28 | api_key=$(exec_on conjur conjurctl role retrieve-key conjur:user:admin | tr -d '\r') 29 | 30 | # Export values needed for tests to access Conjur instance 31 | export CONJUR_AUTHN_API_KEY="$api_key" 32 | } 33 | 34 | main 35 | -------------------------------------------------------------------------------- /conjurapi/authn/oidc_authenticator_test.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestOidcAuthenticator_RefreshToken(t *testing.T) { 10 | // Test that the RefreshToken method calls the Authenticate method 11 | t.Run("Calls Authenticate", func(t *testing.T) { 12 | authenticator := OidcAuthenticator{ 13 | Authenticate: func(code, nonce, code_verifier string) ([]byte, error) { 14 | return []byte("token"), nil 15 | }, 16 | } 17 | 18 | token, err := authenticator.RefreshToken() 19 | 20 | assert.NoError(t, err) 21 | assert.Equal(t, []byte("token"), token) 22 | }) 23 | } 24 | 25 | func TestOidcAuthenticator_NeedsTokenRefresh(t *testing.T) { 26 | t.Run("Returns false", func(t *testing.T) { 27 | // Test that the NeedsTokenRefresh method always returns false 28 | authenticator := OidcAuthenticator{} 29 | 30 | assert.False(t, authenticator.NeedsTokenRefresh()) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /conjurapi/authn/oidc_authenticator.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | // OidcAuthenticator handles authentication to Conjur using the authn-oidc authenticator. 4 | // It uses an OIDC authorization code flow to get a Conjur access token. 5 | type OidcAuthenticator struct { 6 | Code string 7 | Nonce string 8 | CodeVerifier string 9 | Authenticate func(code, nonce, code_verifier string) ([]byte, error) 10 | } 11 | 12 | func (a *OidcAuthenticator) RefreshToken() ([]byte, error) { 13 | return a.Authenticate(a.Code, a.Nonce, a.CodeVerifier) 14 | } 15 | 16 | func (a *OidcAuthenticator) NeedsTokenRefresh() bool { 17 | return false 18 | } 19 | 20 | type OidcTokenAuthenticator struct { 21 | Token string 22 | Authenticate func(token string) ([]byte, error) 23 | } 24 | 25 | func (a *OidcTokenAuthenticator) RefreshToken() ([]byte, error) { 26 | return a.Authenticate(a.Token) 27 | } 28 | 29 | func (a *OidcTokenAuthenticator) NeedsTokenRefresh() bool { 30 | return false 31 | } 32 | -------------------------------------------------------------------------------- /conjurapi/authn/wait_for_file_test.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_waitForTextFile(t *testing.T) { 12 | t.Run("Times out for non-existent filename", func(t *testing.T) { 13 | bytes, err := waitForTextFile("path/to/non-existent/file", time.After(0)) 14 | assert.Error(t, err) 15 | assert.Equal(t, err.Error(), "Operation waitForTextFile timed out.") 16 | assert.Nil(t, bytes) 17 | }) 18 | 19 | t.Run("Returns bytes for eventually existent filename", func(t *testing.T) { 20 | file_to_exist, _ := os.CreateTemp("", "existent-file") 21 | file_to_exist_name := file_to_exist.Name() 22 | 23 | os.Remove(file_to_exist_name) 24 | go func() { 25 | os.WriteFile(file_to_exist_name, []byte("some random stuff"), 0600) 26 | }() 27 | defer os.Remove(file_to_exist_name) 28 | 29 | bytes, err := waitForTextFile(file_to_exist_name, nil) 30 | 31 | assert.NoError(t, err) 32 | assert.Equal(t, "some random stuff", string(bytes)) 33 | 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /conjurapi/authn/token_file_authenticator.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "os" 5 | "time" 6 | ) 7 | 8 | // TokenFileAuthenticator handles authentication to Conjur where a Conjur access token is read from a file. 9 | type TokenFileAuthenticator struct { 10 | TokenFile string `env:"CONJUR_AUTHN_TOKEN_FILE"` 11 | mTime time.Time 12 | MaxWaitTime time.Duration 13 | } 14 | 15 | // RefreshToken reads and returns the Conjur access token from the specified file. 16 | func (a *TokenFileAuthenticator) RefreshToken() ([]byte, error) { 17 | // TODO: is this implementation concurrent ? 18 | maxWaitTime := a.MaxWaitTime 19 | var timeout <-chan time.Time 20 | if maxWaitTime == -1 { 21 | timeout = nil 22 | } else { 23 | timeout = time.After(a.MaxWaitTime) 24 | } 25 | 26 | bytes, err := waitForTextFile(a.TokenFile, timeout) 27 | if err == nil { 28 | fi, _ := os.Stat(a.TokenFile) 29 | a.mTime = fi.ModTime() 30 | } 31 | return bytes, err 32 | } 33 | 34 | // NeedsTokenRefresh checks if the token file has been modified since the last read. 35 | func (a *TokenFileAuthenticator) NeedsTokenRefresh() bool { 36 | fi, _ := os.Stat(a.TokenFile) 37 | return a.mTime != fi.ModTime() 38 | } 39 | -------------------------------------------------------------------------------- /conjurapi/logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // ApiLog is a separate logrus logger for the API. The destination for 11 | // its messages is controlled by the environment variable 12 | // CONJURAPI_LOG. CONJRAPI_LOG can be "stdout", "stderr", or the path 13 | // to a file. If it's a path, the file's contents will be overwritten 14 | // with new messages. If the environment variable is not set, logging 15 | // is disabled. 16 | var ApiLog = logrus.New() 17 | var fatalFn = logrus.Fatalf 18 | 19 | func init() { 20 | initLogger() 21 | } 22 | 23 | func initLogger() { 24 | dest, ok := os.LookupEnv("CONJURAPI_LOG") 25 | if !ok { 26 | return 27 | } 28 | 29 | var ( 30 | out io.Writer 31 | err error 32 | ) 33 | switch dest { 34 | case "stdout": 35 | out = os.Stdout 36 | case "stderr": 37 | out = os.Stderr 38 | default: 39 | out, err = os.OpenFile(dest, os.O_CREATE|os.O_WRONLY, 0644) 40 | if err != nil { 41 | fatalFn("Failed to open %s: %v", dest, err.Error()) 42 | } 43 | logrus.Infof("Logging to %s", dest) 44 | } 45 | 46 | ApiLog.Out = out 47 | ApiLog.Level = logrus.DebugLevel 48 | } 49 | -------------------------------------------------------------------------------- /conjurapi/version_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidateMinVersion(t *testing.T) { 10 | tests := []struct { 11 | actualVersion string 12 | minVersion string 13 | expectedError string 14 | }{ 15 | {"1.0.0", "1.0.0", ""}, 16 | {"1.0.1", "1.0.0", ""}, 17 | {"1.1.0", "1.0.0", ""}, 18 | {"2.0.0", "1.0.0", ""}, 19 | {"1.0.0", "1.0.1", "Conjur version 1.0.0 is less than the minimum required version 1.0.1"}, 20 | {"1.0.0", "2.0.0", "Conjur version 1.0.0 is less than the minimum required version 2.0.0"}, 21 | {"invalid", "1.0.0", "failed to parse server version: invalid semantic version"}, 22 | {"1.0.0", "invalid", "failed to parse minimum version: invalid semantic version"}, 23 | {"1.21.1-359", "1.21.1", ""}, 24 | {"1.21.0-359", "1.21.1-359", "Conjur version 1.21.0-359 is less than the minimum required version 1.21.1-359"}, 25 | {"1.21.1-359", "1.21.0-359", ""}, 26 | } 27 | 28 | for _, test := range tests { 29 | err := validateMinVersion(test.actualVersion, test.minVersion) 30 | if test.expectedError == "" { 31 | assert.NoError(t, err) 32 | } else { 33 | assert.EqualError(t, err, test.expectedError) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bin/get_gcp_token.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeo pipefail 3 | 4 | HOST_ID="$1" 5 | ACCOUNT="$2" 6 | OUTPUT_DIR="$3" 7 | 8 | BASE_URL="http://metadata.google.internal/computeMetadata/v1" 9 | IDENTITY_URL="$BASE_URL/instance/service-accounts/default/identity" 10 | PROJECT_ID_URL="$BASE_URL/project/project-id" 11 | METADATA_FLAVOR_HEADER="Metadata-Flavor: Google" 12 | 13 | # Check if account, hostId, and output file are provided 14 | if [[ -z "$ACCOUNT" || -z "$HOST_ID" || -z "$OUTPUT_DIR" ]]; then 15 | echo "Usage: $0 " 16 | exit 1 17 | fi 18 | 19 | rm -rf "$OUTPUT_DIR" 2>/dev/null 20 | mkdir -p "$OUTPUT_DIR" 21 | 22 | # Build audience parameter 23 | AUDIENCE="conjur/$ACCOUNT/$HOST_ID" 24 | 25 | # Make the request to the metadata server 26 | TOKEN=$(curl -s "$IDENTITY_URL?audience=$AUDIENCE&format=full" -H "$METADATA_FLAVOR_HEADER") 27 | 28 | # Check if the request was successful 29 | if [[ $? -ne 0 || -z "$TOKEN" ]]; then 30 | echo "Failed to fetch the token." 31 | exit 1 32 | fi 33 | 34 | # Store the token in a file 35 | echo "$TOKEN" > "$OUTPUT_DIR/token" 36 | echo "Token saved to $OUTPUT_DIR/token" 37 | 38 | # Store the project ID in a file 39 | GCP_PROJECT=$(curl -s "$PROJECT_ID_URL" -H "$METADATA_FLAVOR_HEADER") 40 | echo "$GCP_PROJECT" > "$OUTPUT_DIR/project-id" 41 | echo "Project ID saved to $OUTPUT_DIR/project-id" -------------------------------------------------------------------------------- /conjurapi/version.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "fmt" 5 | 6 | semver "github.com/Masterminds/semver/v3" 7 | ) 8 | 9 | // VerifyMinServerVersion checks if the server version is at least a certain version, using semantic versioning. 10 | func (c *Client) VerifyMinServerVersion(minVersion string) error { 11 | if c.conjurVersion == "" { 12 | serverVersion, err := c.ServerVersion() 13 | if err != nil { 14 | return err 15 | } 16 | 17 | c.conjurVersion = serverVersion 18 | } 19 | return validateMinVersion(c.conjurVersion, minVersion) 20 | } 21 | 22 | // Validates that the actual version is at least the minimum version, using semantic versioning. 23 | func validateMinVersion(actualVersion string, minVersion string) error { 24 | conjurVersion, err := semver.NewVersion(actualVersion) 25 | if err != nil { 26 | return fmt.Errorf("failed to parse server version: %s", err) 27 | } 28 | 29 | minConjurVersion, err := semver.NewVersion(minVersion) 30 | if err != nil { 31 | return fmt.Errorf("failed to parse minimum version: %s", err) 32 | } 33 | 34 | // Ignore version suffixes (eg. 1.21.1-359) as we use them differently in the Conjur versioning scheme. 35 | // In SemVer, the suffix is considered a pre-release version, but in Conjur, it is used as a build version. 36 | simplifiedVersion, _ := conjurVersion.SetPrerelease("") 37 | 38 | if simplifiedVersion.LessThan(minConjurVersion) { 39 | return fmt.Errorf("Conjur version %s is less than the minimum required version %s", conjurVersion, minConjurVersion) 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /conjurapi/response/error.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/cyberark/conjur-api-go/conjurapi/logging" 10 | ) 11 | 12 | type ConjurError struct { 13 | Code int 14 | Message string 15 | Details *ConjurErrorDetails `json:"error"` 16 | } 17 | 18 | type ConjurErrorDetails struct { 19 | Message string 20 | Code string 21 | Target string 22 | Details map[string]interface{} 23 | } 24 | 25 | func NewConjurError(resp *http.Response) error { 26 | defer resp.Body.Close() 27 | body, err := io.ReadAll(resp.Body) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | cerr := ConjurError{} 33 | cerr.Code = resp.StatusCode 34 | err = json.Unmarshal(body, &cerr) 35 | if err != nil { 36 | cerr.Message = strings.TrimSpace(string(body)) 37 | } 38 | 39 | // If the body's empty, use the HTTP status as the message 40 | if cerr.Message == "" { 41 | cerr.Message = resp.Status 42 | } 43 | 44 | return &cerr 45 | } 46 | 47 | func (cerr *ConjurError) Error() string { 48 | logging.ApiLog.Debugf("cerr.Details: %+v, cerr.Message: %+v\n", cerr.Details, cerr.Message) 49 | 50 | var b strings.Builder 51 | 52 | hasMessage := cerr.Message != "" 53 | hasDetails := cerr.Details != nil && cerr.Details.Message != "" 54 | 55 | if hasMessage { 56 | b.WriteString(cerr.Message) 57 | 58 | // If there's both a message and details, separate them with a period and space 59 | if hasDetails { 60 | b.WriteString(". ") 61 | } 62 | } 63 | 64 | if hasDetails { 65 | b.WriteString(cerr.Details.Message + ".") 66 | } 67 | 68 | return b.String() 69 | } 70 | -------------------------------------------------------------------------------- /conjurapi/authn/api_key_authenticator_test.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAPIKeyAuthenticator_RefreshToken(t *testing.T) { 11 | var login string 12 | apiKey := "valid-api-key" 13 | authenticate := func(loginPair LoginPair) ([]byte, error) { 14 | if loginPair.Login == "valid-login" && loginPair.APIKey == "valid-api-key" { 15 | return []byte("data"), nil 16 | } else { 17 | return nil, fmt.Errorf("401 Invalid") 18 | } 19 | } 20 | 21 | t.Run("Given valid credentials returns the token bytes", func(t *testing.T) { 22 | // file deepcode ignore NoHardcodedCredentials/test: This is a test file 23 | login := "valid-login" 24 | authenticator := APIKeyAuthenticator{ 25 | Authenticate: authenticate, 26 | LoginPair: LoginPair{ 27 | Login: login, 28 | APIKey: apiKey, 29 | }, 30 | } 31 | 32 | token, err := authenticator.RefreshToken() 33 | 34 | assert.NoError(t, err) 35 | assert.Contains(t, string(token), "data") 36 | }) 37 | 38 | t.Run("Given invalid credentials returns nil with error", func(t *testing.T) { 39 | login = "invalid-login" 40 | authenticator := APIKeyAuthenticator{ 41 | Authenticate: authenticate, 42 | LoginPair: LoginPair{ 43 | Login: login, 44 | APIKey: apiKey, 45 | }, 46 | } 47 | 48 | token, err := authenticator.RefreshToken() 49 | 50 | assert.Nil(t, token) 51 | assert.Error(t, err) 52 | assert.Contains(t, err.Error(), "401") 53 | }) 54 | } 55 | 56 | func TestAPIKeyAuthenticator_NeedsTokenRefresh(t *testing.T) { 57 | t.Run("Returns false", func(t *testing.T) { 58 | authenticator := APIKeyAuthenticator{} 59 | 60 | assert.False(t, authenticator.NeedsTokenRefresh()) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /conjurapi/authn/jwt_authenticator.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/cyberark/conjur-api-go/conjurapi/logging" 8 | ) 9 | 10 | type JWTAuthenticator struct { 11 | JWT string 12 | JWTFilePath string 13 | HostID string 14 | Authenticate func(jwt, hostId string) ([]byte, error) 15 | } 16 | 17 | const k8sJWTPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" 18 | 19 | func (a *JWTAuthenticator) RefreshToken() ([]byte, error) { 20 | err := a.RefreshJWT() 21 | if err != nil { 22 | return nil, fmt.Errorf("Failed to refresh JWT: %v", err) 23 | } 24 | return a.Authenticate(a.JWT, a.HostID) 25 | } 26 | 27 | func (a *JWTAuthenticator) NeedsTokenRefresh() bool { 28 | return false 29 | } 30 | 31 | func (a *JWTAuthenticator) RefreshJWT() error { 32 | // If a JWT token is already set or retrieved, do nothing. 33 | if a.JWT != "" { 34 | logging.ApiLog.Debugf("Using stored JWT") 35 | return nil 36 | } 37 | 38 | // If a token file path is provided, read the JWT token from the file. 39 | // Otherwise, read the token from the default Kubernetes service account path. 40 | var jwtFilePath string 41 | if a.JWTFilePath != "" { 42 | logging.ApiLog.Debugf("Reading JWT from %s", a.JWTFilePath) 43 | jwtFilePath = a.JWTFilePath 44 | } else { 45 | jwtFilePath = k8sJWTPath 46 | logging.ApiLog.Debugf("No JWT file path set. Attempting to ready JWT from %s", jwtFilePath) 47 | } 48 | 49 | token, err := readJWTFromFile(jwtFilePath) 50 | if err != nil { 51 | return err 52 | } 53 | a.JWT = token 54 | return nil 55 | } 56 | 57 | func readJWTFromFile(filePath string) (string, error) { 58 | bytes, err := os.ReadFile(filePath) 59 | if err != nil { 60 | return "", err 61 | } 62 | return string(bytes), nil 63 | } 64 | -------------------------------------------------------------------------------- /conjurapi/router_url.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/cyberark/conjur-api-go/conjurapi/logging" 10 | ) 11 | 12 | // ConjurCloudSuffixes is a list of all possible Secrets Manager SaaS URL suffixes. 13 | var ConjurCloudSuffixes = []string{ 14 | ".cyberark.cloud", 15 | ".integration-cyberark.cloud", 16 | ".test-cyberark.cloud", 17 | ".dev-cyberark.cloud", 18 | ".cyberark-everest-integdev.cloud", 19 | ".cyberark-everest-pre-prod.cloud", 20 | ".sandbox-cyberark.cloud", 21 | ".pt-cyberark.cloud", 22 | } 23 | 24 | // ConjurCloudRegexp is a regex pattern that matches all possible Secrets Manager SaaS URLs. 25 | var ConjurCloudRegexp = regexp.MustCompile("(\\.secretsmgr|-secretsmanager)" + strings.Join(ConjurCloudSuffixes, "|")) 26 | 27 | type routerURL string 28 | 29 | func makeRouterURL(base string, components ...string) routerURL { 30 | urlBase := normalizeBaseURL(base) 31 | urlPath := path.Join(components...) 32 | urlPath = strings.TrimPrefix(urlPath, "/") 33 | return routerURL(urlBase + "/" + urlPath) 34 | } 35 | 36 | func (u routerURL) withFormattedQuery(queryFormat string, queryArgs ...interface{}) routerURL { 37 | query := fmt.Sprintf(queryFormat, queryArgs...) 38 | return routerURL(strings.Join([]string{string(u), query}, "?")) 39 | } 40 | 41 | func (u routerURL) withQuery(query string) routerURL { 42 | return routerURL(strings.Join([]string{string(u), query}, "?")) 43 | } 44 | 45 | func (u routerURL) String() string { 46 | return string(u) 47 | } 48 | 49 | func normalizeBaseURL(baseURL string) string { 50 | url := strings.TrimSuffix(baseURL, "/") 51 | if isConjurCloudURL(url) && !strings.Contains(url, "/api") { 52 | logging.ApiLog.Info("Detected Secrets Manager SaaS URL, adding '/api' prefix") 53 | return url + "/api" 54 | } 55 | 56 | return url 57 | } 58 | 59 | func isConjurCloudURL(baseURL string) bool { 60 | return ConjurCloudRegexp.MatchString(baseURL) 61 | } 62 | -------------------------------------------------------------------------------- /bin/utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export compose_file="../docker-compose.yml" 4 | 5 | function announce() { 6 | echo " 7 | ================================ 8 | ${1} 9 | ================================ 10 | " 11 | } 12 | 13 | exec_on() { 14 | local container="$1"; shift 15 | 16 | docker exec "$(docker compose ps -q $container)" "$@" 17 | } 18 | 19 | function teardown { 20 | docker compose logs conjur > "../output/$GO_VERSION/conjur-logs.txt" 2>&1 || true 21 | docker compose down -v --remove-orphans 22 | unset API_PKGS 23 | unset API_TESTS 24 | } 25 | 26 | failed() { 27 | announce "TESTS FAILED" 28 | # docker compose logs conjur || true 29 | teardown 30 | exit 1 31 | } 32 | 33 | # Docker program name rules: must consist only of lowercase alphanumeric characters, 34 | # hyphens, and underscores as well as start with a letter or number 35 | function project_nameable() { 36 | local split=$(echo "$1" | tr ',.@/' '-') 37 | local lower=$(echo "$split" | tr '[:upper:]' '[:lower:]') 38 | local shrnk=$(echo "$lower" | tr -d 'aeiou') 39 | echo "$shrnk" 40 | } 41 | 42 | # Starts a temporary JWT issuer service and exports the public keys and JWT token 43 | # NOTE: We curl from a container in the compose network so we don't have to map a 44 | # host port - otherwise a port collision may occur when running tests in parallel 45 | function init_jwt_server() { 46 | pushd .. 47 | docker compose up -d mock-jwt-server 48 | while true; do 49 | export JWT=$(docker compose run -T --rm --no-deps --entrypoint /bin/bash conjur -c "curl http://mock-jwt-server:8080/token" | jq -r .token) 50 | if [[ -n "$JWT" ]]; then 51 | break 52 | fi 53 | echo "Waiting for mock JWT server to be ready..." 54 | sleep 1 55 | done 56 | export PUBLIC_KEYS=$(docker compose run -T --rm --no-deps --entrypoint /bin/bash conjur -c "curl http://mock-jwt-server:8080/.well-known/jwks.json") 57 | docker compose down mock-jwt-server 58 | popd 59 | } 60 | -------------------------------------------------------------------------------- /conjurapi/router_url_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_makeRouterURL(t *testing.T) { 10 | t.Run("makeRouterURL removes extra '/'s between base and components", func(t *testing.T) { 11 | urlWithPath := routerURL("http://some.host/some/path") 12 | urlWithSubPath := routerURL("http://some.host/path/to/something/subpath/to/another") 13 | 14 | assert.Equal(t, urlWithPath, makeRouterURL("http://some.host", "some/path")) 15 | assert.Equal(t, urlWithPath, makeRouterURL("http://some.host/", "//some/path")) 16 | assert.Equal(t, urlWithPath, makeRouterURL("http://some.host/", "some//path")) 17 | assert.Equal(t, urlWithPath, makeRouterURL("http://some.host/", "some/path//")) 18 | assert.Equal(t, urlWithPath, makeRouterURL("http://some.host", "//some/path")) 19 | assert.Equal(t, urlWithPath, makeRouterURL("http://some.host", "some//path")) 20 | assert.Equal(t, urlWithPath, makeRouterURL("http://some.host", "some/path//")) 21 | assert.Equal(t, urlWithSubPath, makeRouterURL("http://some.host/path/to/something/", "//subpath//to//another")) 22 | }) 23 | 24 | t.Run("makeRouterURL handles Secrets Manager SaaS base URL", func(t *testing.T) { 25 | cloudUrlWithPath := routerURL("http://some.host.secretsmgr.cyberark.cloud/api/some/path") 26 | cloudUrlWithSubPath := routerURL("http://some.host.secretsmgr.cyberark.cloud/api/some/path/subpath/to/another") 27 | 28 | t.Run("when '/api' prefix is not provided", func(t *testing.T) { 29 | assert.Equal(t, cloudUrlWithPath, makeRouterURL("http://some.host.secretsmgr.cyberark.cloud", "some/path")) 30 | }) 31 | 32 | t.Run("when '/api' prefix is provided", func(t *testing.T) { 33 | assert.Equal(t, cloudUrlWithPath, makeRouterURL("http://some.host.secretsmgr.cyberark.cloud/api", "some/path")) 34 | }) 35 | 36 | t.Run("when adding subpaths", func(t *testing.T) { 37 | assert.Equal(t, cloudUrlWithSubPath, makeRouterURL("http://some.host.secretsmgr.cyberark.cloud/api/some/path/", "subpath/to/another")) 38 | }) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /conjurapi/storage.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cyberark/conjur-api-go/conjurapi/logging" 7 | "github.com/cyberark/conjur-api-go/conjurapi/storage" 8 | ) 9 | 10 | const ( 11 | CredentialStorageFile = "file" 12 | CredentialStorageKeyring = "keyring" 13 | CredentialStorageNone = "none" 14 | ) 15 | 16 | func createStorageProvider(config Config) (CredentialStorageProvider, error) { 17 | if config.CredentialStorage == "" { 18 | config.CredentialStorage = getDefaultCredentialStorage() 19 | logging.ApiLog.Debugf("No credential storage specified, defaulting to %s", config.CredentialStorage) 20 | } 21 | 22 | switch config.CredentialStorage { 23 | case CredentialStorageFile: 24 | provider, err := storage.NewNetrcStorageProvider( 25 | config.NetRCPath, 26 | getMachineName(config), 27 | ) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return provider, nil 32 | case CredentialStorageKeyring: 33 | if !storage.IsKeyringAvailable() { 34 | return nil, fmt.Errorf("Keyring is not available") 35 | } 36 | 37 | return storage.NewKeyringStorageProvider( 38 | getMachineName(config), 39 | ), nil 40 | case CredentialStorageNone: 41 | // Don't store credentials 42 | logging.ApiLog.Debugf("Not storing credentials") 43 | return nil, nil 44 | default: 45 | return nil, fmt.Errorf("Unknown credential storage type") 46 | } 47 | } 48 | 49 | // getMachineName returns the machine name to use in the .netrc file or other credential storage. 50 | // It contains the appliance URL and the path to the authentication endpoint. 51 | func getMachineName(config Config) string { 52 | if config.AuthnType != "" && config.AuthnType != "authn" { 53 | authnType := fmt.Sprintf("authn-%s", config.AuthnType) 54 | return fmt.Sprintf("%s/%s/%s", config.ApplianceURL, authnType, config.ServiceID) 55 | } 56 | 57 | return config.ApplianceURL + "/authn" 58 | } 59 | 60 | func getDefaultCredentialStorage() string { 61 | if storage.IsKeyringAvailable() { 62 | return CredentialStorageKeyring 63 | } 64 | 65 | return CredentialStorageFile 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cyberark/conjur-api-go 2 | 3 | // This version has to be the lowest of the versions that we run tests with. Currently 4 | // we test with 1.24 and 1.25 (See Jenkinsfile) so this needs to be 1.24. 5 | go 1.24.0 6 | 7 | require ( 8 | github.com/Masterminds/semver/v3 v3.4.0 9 | github.com/aws/aws-sdk-go-v2 v1.39.1 10 | github.com/aws/aws-sdk-go-v2/config v1.31.10 11 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d 12 | github.com/sirupsen/logrus v1.9.3 13 | github.com/stretchr/testify v1.11.1 14 | github.com/zalando/go-keyring v0.2.6 15 | go.yaml.in/yaml/v3 v3.0.4 16 | ) 17 | 18 | require ( 19 | al.essio.dev/pkg/shellescape v1.6.0 // indirect 20 | github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect 21 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect 22 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect 23 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect 24 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect 30 | github.com/aws/smithy-go v1.23.0 // indirect 31 | github.com/danieljoos/wincred v1.2.2 // indirect 32 | github.com/davecgh/go-spew v1.1.1 // indirect 33 | github.com/godbus/dbus/v5 v5.1.0 // indirect 34 | github.com/pmezard/go-difflib v1.0.0 // indirect 35 | golang.org/x/sys v0.26.0 // indirect 36 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | 40 | replace gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c => gopkg.in/yaml.v3 v3.0.1 41 | 42 | replace golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 => golang.org/x/sys v0.8.0 43 | 44 | replace golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c => golang.org/x/sys v0.8.0 45 | -------------------------------------------------------------------------------- /conjurapi/environment.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strings" 7 | 8 | "github.com/cyberark/conjur-api-go/conjurapi/logging" 9 | ) 10 | 11 | // EnvironmentType represents the type of Secrets Manager environment. 12 | type EnvironmentType string 13 | 14 | const ( 15 | // EnvironmentSaaS represents the Secrets Manager SaaS environment. 16 | EnvironmentSaaS EnvironmentType = "saas" 17 | // EnvironmentSH represents the Secrets Manager Self-Hosted environment. 18 | EnvironmentSH EnvironmentType = "self-hosted" 19 | // EnvironmentOSS represents the Conjur Open Source environment. 20 | EnvironmentOSS EnvironmentType = "oss" 21 | ) 22 | 23 | // SupportedEnvironments lists all supported environment types. 24 | var SupportedEnvironments = []string{string(EnvironmentSaaS), string(EnvironmentSH), string(EnvironmentOSS)} 25 | 26 | // String returns the string representation of the EnvironmentType. 27 | func (e *EnvironmentType) String() string { 28 | return string(*e) 29 | } 30 | 31 | // FullName returns the full descriptive name of the EnvironmentType. 32 | func (e *EnvironmentType) FullName() string { 33 | switch *e { 34 | case EnvironmentSaaS: 35 | return "Secrets Manager SaaS" 36 | case EnvironmentSH: 37 | return "Secrets Manager Self-Hosted" 38 | case EnvironmentOSS: 39 | return "Conjur Open Source" 40 | default: 41 | return "Unknown Environment" 42 | } 43 | } 44 | 45 | // Set sets the EnvironmentType based on the provided string value. 46 | func (e *EnvironmentType) Set(value string) error { 47 | switch value { 48 | case string(EnvironmentSH), "CE", "enterprise": 49 | *e = EnvironmentSH 50 | case string(EnvironmentOSS), "OSS", "open-source": 51 | *e = EnvironmentOSS 52 | case string(EnvironmentSaaS), "cloud", "CC": 53 | *e = EnvironmentSaaS 54 | default: 55 | return fmt.Errorf("invalid value environment: %s, allowed values %v", value, SupportedEnvironments) 56 | } 57 | return nil 58 | } 59 | 60 | // Type returns the type of the EnvironmentType for flag parsing. 61 | func (e *EnvironmentType) Type() string { 62 | return "string" 63 | } 64 | 65 | func environmentIsSupported(environment string) bool { 66 | return slices.Contains(SupportedEnvironments, strings.ToLower(environment)) 67 | } 68 | 69 | func defaultEnvironment(url string, showLog bool) EnvironmentType { 70 | if isConjurCloudURL(url) { 71 | if showLog { 72 | logging.ApiLog.Info("Detected Secrets Manager SaaS URL, setting 'Environment' to 'saas'") 73 | } 74 | return EnvironmentSaaS 75 | } 76 | if showLog { 77 | logging.ApiLog.Info("'Environment' not specified, setting to 'self-hosted'") 78 | } 79 | return EnvironmentSH 80 | } 81 | -------------------------------------------------------------------------------- /conjurapi/logging/logging_test.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "testing" 8 | 9 | "github.com/sirupsen/logrus" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestInitLogger(t *testing.T) { 14 | t.Run("CONJURAPI_LOG is not set", func(t *testing.T) { 15 | os.Unsetenv("CONJURAPI_LOG") 16 | initLogger() 17 | // Defaults to Stderr with Info level 18 | assert.Equal(t, os.Stderr, ApiLog.Out) 19 | assert.Equal(t, logrus.InfoLevel, ApiLog.Level) 20 | }) 21 | 22 | t.Run("stdout", func(t *testing.T) { 23 | os.Setenv("CONJURAPI_LOG", "stdout") 24 | initLogger() 25 | assert.Equal(t, os.Stdout, ApiLog.Out) 26 | assert.Equal(t, logrus.DebugLevel, ApiLog.Level) 27 | }) 28 | 29 | t.Run("stderr", func(t *testing.T) { 30 | os.Setenv("CONJURAPI_LOG", "stderr") 31 | initLogger() 32 | assert.Equal(t, os.Stderr, ApiLog.Out) 33 | assert.Equal(t, logrus.DebugLevel, ApiLog.Level) 34 | }) 35 | 36 | t.Run("file", func(t *testing.T) { 37 | tmpFile := t.TempDir() + "/logfile.log" 38 | os.Setenv("CONJURAPI_LOG", tmpFile) 39 | initLogger() 40 | assertFileExists(t, tmpFile) 41 | assert.Equal(t, logrus.DebugLevel, ApiLog.Level) 42 | }) 43 | 44 | t.Run("file in nonexistent directory", func(t *testing.T) { 45 | tmpFile := "/nonexistent/logfile.log" 46 | fatalCalled := false 47 | 48 | // Mock the logrus.Fatalf function 49 | fatalFn = func(format string, args ...interface{}) { 50 | fatalCalled = true 51 | assert.Contains(t, format, "Failed to open") 52 | assert.Len(t, args, 2) 53 | assert.Equal(t, args[0], tmpFile) 54 | assert.Contains(t, args[1], "no such file or directory") 55 | } 56 | 57 | os.Setenv("CONJURAPI_LOG", tmpFile) 58 | initLogger() 59 | assert.True(t, fatalCalled) 60 | }) 61 | } 62 | 63 | func assertFileExists(t *testing.T, filePath string) { 64 | _, err := os.Stat(filePath) 65 | assert.False(t, os.IsNotExist(err), "Expected file to exist: %s", filePath) 66 | } 67 | 68 | func TestApiLog(t *testing.T) { 69 | // Redirect logrus output to a buffer 70 | var buf bytes.Buffer 71 | ApiLog = logrus.New() 72 | ApiLog.Out = &buf 73 | ApiLog.Level = logrus.DebugLevel 74 | 75 | // Test logging 76 | ApiLog.Debug("Debug message") 77 | ApiLog.Info("Info message") 78 | ApiLog.Warn("Warning message") 79 | ApiLog.Error("Error message") 80 | 81 | // Read the buffer contents 82 | logOutput, err := io.ReadAll(&buf) 83 | assert.NoError(t, err) 84 | 85 | assert.Contains(t, string(logOutput), "Debug message") 86 | assert.Contains(t, string(logOutput), "Info message") 87 | assert.Contains(t, string(logOutput), "Warning message") 88 | assert.Contains(t, string(logOutput), "Error message") 89 | } 90 | -------------------------------------------------------------------------------- /conjurapi/authn/iam_authenticator_test.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestBuildRequest(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | config aws.Config 15 | expectError string 16 | expectHost string 17 | }{ 18 | { 19 | name: "Valid region", 20 | config: aws.Config{ 21 | Region: "us-west-2", 22 | }, 23 | expectError: "", 24 | expectHost: "sts.us-west-2.amazonaws.com", 25 | }, 26 | { 27 | name: "Global region", 28 | config: aws.Config{ 29 | Region: "global", 30 | }, 31 | expectError: "", 32 | expectHost: "sts.amazonaws.com", 33 | }, 34 | { 35 | name: "Empty region", 36 | config: aws.Config{ 37 | Region: "", 38 | }, 39 | expectError: "Invalid AWS region", 40 | }, 41 | { 42 | name: "Invalid region", 43 | config: aws.Config{ 44 | Region: "invalid?region", 45 | }, 46 | expectError: "Invalid AWS region", 47 | expectHost: "", 48 | }, 49 | } 50 | 51 | for _, tc := range testCases { 52 | t.Run(tc.name, func(t *testing.T) { 53 | req, err := buildRequest(tc.config) 54 | if tc.expectError != "" { 55 | require.Error(t, err) 56 | assert.Contains(t, err.Error(), tc.expectError) 57 | } else { 58 | require.NoError(t, err) 59 | assert.NotNil(t, req) 60 | 61 | assert.Equal(t, tc.expectHost, req.Host) 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func TestIsValidAWSRegion(t *testing.T) { 68 | testCases := []struct { 69 | region string 70 | expectValid bool 71 | }{ 72 | {"us-east-1", true}, 73 | {"us-west-2", true}, 74 | {"us-gov-west-1", true}, 75 | {"global", true}, 76 | {"invalid-region", false}, 77 | {"us_east_1", false}, 78 | {"us-east-1!", false}, 79 | {"foo?bar", false}, 80 | } 81 | 82 | for _, tc := range testCases { 83 | t.Run(tc.region, func(t *testing.T) { 84 | isValid := isValidAWSRegion(tc.region) 85 | assert.Equal(t, tc.expectValid, isValid) 86 | }) 87 | } 88 | } 89 | 90 | func TestIsValidAWSHost(t *testing.T) { 91 | testCases := []struct { 92 | host string 93 | expectValid bool 94 | }{ 95 | {"sts.us-east-1.amazonaws.com", true}, 96 | {"sts.us-gov-west-1.amazonaws.com", true}, 97 | {"sts.us-east-1.amazonaws.com.malware.com", false}, 98 | } 99 | 100 | for _, tc := range testCases { 101 | t.Run(tc.host, func(t *testing.T) { 102 | isValid := isValidAWSHost(tc.host) 103 | assert.Equal(t, tc.expectValid, isValid) 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:15 4 | environment: 5 | # To avoid the following error: 6 | # 7 | # Error: Database is uninitialized and superuser password is not 8 | # specified. You must specify POSTGRES_PASSWORD for the superuser. Use 9 | # "-e POSTGRES_PASSWORD=password" to set it in "docker run". 10 | # 11 | # You may also use POSTGRES_HOST_AUTH_METHOD=trust to allow all 12 | # connections without a password. This is *not* recommended. See 13 | # PostgreSQL documentation about "trust" 14 | POSTGRES_HOST_AUTH_METHOD: trust 15 | 16 | conjur: 17 | image: ${REGISTRY_URL:-docker.io}/cyberark/conjur:edge 18 | command: server -a conjur 19 | environment: 20 | DATABASE_URL: postgres://postgres@postgres/postgres 21 | CONJUR_DATA_KEY: 22 | RAILS_ENV: development 23 | # Enable dynamic secrets for the Issuers API 24 | CONJUR_FEATURE_DYNAMIC_SECRETS_ENABLED: true 25 | depends_on: 26 | - postgres 27 | 28 | test-1.24: 29 | build: 30 | context: . 31 | args: 32 | FROM_IMAGE: "golang:1.24" 33 | ports: 34 | - 8080 35 | depends_on: 36 | - conjur 37 | volumes: 38 | - ./output:/conjur-api-go/output 39 | environment: 40 | CONJUR_DATA_KEY: 41 | CONJUR_APPLIANCE_URL: http://conjur 42 | CONJUR_ACCOUNT: conjur 43 | CONJUR_AUTHN_LOGIN: admin 44 | CONJUR_AUTHN_API_KEY: 45 | GO_VERSION: 46 | 47 | test-1.25: 48 | build: 49 | context: . 50 | args: 51 | FROM_IMAGE: "golang:1.25" 52 | ports: 53 | - 8080 54 | depends_on: 55 | - conjur 56 | volumes: 57 | - ./output:/conjur-api-go/output 58 | environment: 59 | CONJUR_DATA_KEY: 60 | CONJUR_APPLIANCE_URL: http://conjur 61 | CONJUR_ACCOUNT: conjur 62 | CONJUR_AUTHN_LOGIN: admin 63 | CONJUR_AUTHN_API_KEY: 64 | GO_VERSION: 65 | 66 | dev: 67 | build: 68 | context: . 69 | args: 70 | FROM_IMAGE: "golang:1.25" 71 | ports: 72 | - 8080 73 | depends_on: 74 | - conjur 75 | volumes: 76 | - .:/conjur-api-go 77 | environment: 78 | CONJUR_DATA_KEY: 79 | CONJUR_APPLIANCE_URL: http://conjur 80 | CONJUR_ACCOUNT: conjur 81 | CONJUR_AUTHN_LOGIN: admin 82 | CONJUR_AUTHN_API_KEY: 83 | entrypoint: sleep 84 | command: infinity 85 | 86 | mock-jwt-server: 87 | image: cyberark/mock-jwt-server:latest 88 | ports: 89 | - 8080 90 | environment: 91 | ISSUER: "jwt-server" 92 | AUDIENCE: "conjur" 93 | SUBJECT: "test-workload" 94 | EMAIL: "workload@example.com" 95 | -------------------------------------------------------------------------------- /conjurapi/storage/keyring_storage_provider.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/cyberark/conjur-api-go/conjurapi/logging" 7 | "github.com/zalando/go-keyring" 8 | ) 9 | 10 | type KeyringStorageProvider struct { 11 | machineName string 12 | } 13 | 14 | var keyring_keys = []string{"login", "password", "authn_token"} 15 | var ErrWritingCredentials = errors.New("unable to write credentials to keyring") 16 | var ErrReadingCredentials = errors.New("unable to read credentials from keyring") 17 | 18 | func NewKeyringStorageProvider(machineName string) *KeyringStorageProvider { 19 | return &KeyringStorageProvider{ 20 | machineName: machineName, 21 | } 22 | } 23 | 24 | // IsKeyringAvailable returns true if the keyring is available on the system 25 | func IsKeyringAvailable() bool { 26 | // Try to get a value. If there's an error other than "not found", then the 27 | // keyring is not available. 28 | _, err := keyring.Get("test", "test") 29 | return err == keyring.ErrNotFound 30 | } 31 | 32 | func (k *KeyringStorageProvider) StoreCredentials(login string, password string) error { 33 | err := keyring.Set(k.machineName, "login", login) 34 | if err != nil { 35 | logging.ApiLog.Debug(err) 36 | return ErrWritingCredentials 37 | } 38 | 39 | err = keyring.Set(k.machineName, "password", password) 40 | if err != nil { 41 | logging.ApiLog.Debug(err) 42 | return ErrWritingCredentials 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (k *KeyringStorageProvider) ReadCredentials() (string, string, error) { 49 | login, err := keyring.Get(k.machineName, "login") 50 | if err != nil && err != keyring.ErrNotFound { 51 | logging.ApiLog.Debug(err) 52 | return "", "", ErrReadingCredentials 53 | } 54 | password, err := keyring.Get(k.machineName, "password") 55 | if err != nil && err != keyring.ErrNotFound { 56 | logging.ApiLog.Debug(err) 57 | return "", "", ErrReadingCredentials 58 | } 59 | return login, password, nil 60 | } 61 | 62 | func (k *KeyringStorageProvider) ReadAuthnToken() ([]byte, error) { 63 | token, err := keyring.Get(k.machineName, "authn_token") 64 | if err != nil && err != keyring.ErrNotFound { 65 | logging.ApiLog.Debug(err) 66 | return nil, ErrReadingCredentials 67 | } 68 | return []byte(token), nil 69 | } 70 | 71 | func (k *KeyringStorageProvider) StoreAuthnToken(token []byte) error { 72 | err := keyring.Set(k.machineName, "authn_token", string(token)) 73 | if err != nil { 74 | logging.ApiLog.Debug(err) 75 | return ErrWritingCredentials 76 | } 77 | return nil 78 | } 79 | 80 | func (k *KeyringStorageProvider) PurgeCredentials() error { 81 | for _, key := range keyring_keys { 82 | err := keyring.Delete(k.machineName, key) 83 | if err != nil { 84 | logging.ApiLog.Debugf("Error when deleting %s from keyring: %s", key, err) 85 | } 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /conjurapi/role.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/cyberark/conjur-api-go/conjurapi/response" 8 | ) 9 | 10 | // RoleExists checks whether or not a role exists 11 | func (c *Client) RoleExists(roleID string) (bool, error) { 12 | req, err := c.RoleRequest(roleID) 13 | if err != nil { 14 | return false, err 15 | } 16 | 17 | resp, err := c.SubmitRequest(req) 18 | if err != nil { 19 | return false, err 20 | } 21 | 22 | if (resp.StatusCode >= 200 && resp.StatusCode < 300) || resp.StatusCode == 403 { 23 | return true, nil 24 | } else if resp.StatusCode == 404 { 25 | return false, nil 26 | } else { 27 | return false, fmt.Errorf("Role exists check failed with HTTP status %d", resp.StatusCode) 28 | } 29 | } 30 | 31 | // Role fetches detailed information about a specific role, including 32 | // the role members 33 | func (c *Client) Role(roleID string) (role map[string]interface{}, err error) { 34 | req, err := c.RoleRequest(roleID) 35 | if err != nil { 36 | return 37 | } 38 | 39 | resp, err := c.SubmitRequest(req) 40 | if err != nil { 41 | return 42 | } 43 | 44 | data, err := response.DataResponse(resp) 45 | if err != nil { 46 | return 47 | } 48 | 49 | role = make(map[string]interface{}) 50 | err = json.Unmarshal(data, &role) 51 | return 52 | } 53 | 54 | // RoleMembers fetches members within a role 55 | func (c *Client) RoleMembers(roleID string) (members []map[string]interface{}, err error) { 56 | req, err := c.RoleMembersRequest(roleID) 57 | if err != nil { 58 | return 59 | } 60 | 61 | resp, err := c.SubmitRequest(req) 62 | if err != nil { 63 | return 64 | } 65 | 66 | data, err := response.DataResponse(resp) 67 | if err != nil { 68 | return 69 | } 70 | 71 | members = make([]map[string]interface{}, 0) 72 | err = json.Unmarshal(data, &members) 73 | return 74 | } 75 | 76 | // RoleMemberships fetches memberships of a role, including 77 | // only roles for which the given ID is a direct member 78 | func (c *Client) RoleMemberships(roleID string) (memberships []map[string]interface{}, err error) { 79 | req, err := c.RoleMembershipsRequest(roleID) 80 | if err != nil { 81 | return 82 | } 83 | 84 | resp, err := c.SubmitRequest(req) 85 | if err != nil { 86 | return 87 | } 88 | 89 | data, err := response.DataResponse(resp) 90 | if err != nil { 91 | return 92 | } 93 | 94 | memberships = make([]map[string]interface{}, 0) 95 | err = json.Unmarshal(data, &memberships) 96 | return 97 | } 98 | 99 | // RoleMembershipsAll fetches all memberships of a role, including 100 | // inherited memberships, returning a list of member IDs 101 | func (c *Client) RoleMembershipsAll(roleID string) (memberships []string, err error) { 102 | req, err := c.RoleMembershipsRequestWithOptions(roleID, true) 103 | if err != nil { 104 | return 105 | } 106 | 107 | resp, err := c.SubmitRequest(req) 108 | if err != nil { 109 | return 110 | } 111 | 112 | data, err := response.DataResponse(resp) 113 | if err != nil { 114 | return 115 | } 116 | 117 | memberships = make([]string, 0) 118 | err = json.Unmarshal(data, &memberships) 119 | return 120 | } 121 | -------------------------------------------------------------------------------- /conjurapi/authn/jwt_authenticator_test.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestJWTAuthenticator_RefreshToken(t *testing.T) { 12 | // Test that the RefreshToken method calls the Authenticate method 13 | t.Run("Calls Authenticate with stored JWT", func(t *testing.T) { 14 | authenticator := JWTAuthenticator{ 15 | Authenticate: func(jwt, hostid string) ([]byte, error) { 16 | assert.Equal(t, "jwt", jwt) 17 | assert.Equal(t, "", hostid) 18 | return []byte("token"), nil 19 | }, 20 | JWT: "jwt", 21 | } 22 | 23 | token, err := authenticator.RefreshToken() 24 | 25 | assert.NoError(t, err) 26 | assert.Equal(t, []byte("token"), token) 27 | }) 28 | 29 | t.Run("Calls Authenticate with JWT from file", func(t *testing.T) { 30 | tempDir := t.TempDir() 31 | err := os.WriteFile(filepath.Join(tempDir, "jwt"), []byte("jwt-content"), 0600) 32 | assert.NoError(t, err) 33 | 34 | authenticator := JWTAuthenticator{ 35 | Authenticate: func(jwt, hostid string) ([]byte, error) { 36 | assert.Equal(t, "jwt-content", jwt) 37 | assert.Equal(t, "host-id", hostid) 38 | return []byte("token"), nil 39 | }, 40 | JWTFilePath: filepath.Join(tempDir, "jwt"), 41 | HostID: "host-id", 42 | } 43 | 44 | token, err := authenticator.RefreshToken() 45 | assert.NoError(t, err) 46 | assert.Equal(t, []byte("token"), token) 47 | }) 48 | 49 | t.Run("Defaults to Kubernetes service account path", func(t *testing.T) { 50 | authenticator := JWTAuthenticator{ 51 | Authenticate: func(jwt, hostid string) ([]byte, error) { 52 | assert.Equal(t, "k8s-jwt-content", jwt) 53 | assert.Equal(t, "", hostid) 54 | return []byte("token"), nil 55 | }, 56 | } 57 | 58 | // Note: this may fail when not running in a container 59 | err := os.MkdirAll(filepath.Dir(k8sJWTPath), 0755) 60 | assert.NoError(t, err) 61 | err = os.WriteFile(k8sJWTPath, []byte("k8s-jwt-content"), 0600) 62 | assert.NoError(t, err) 63 | 64 | token, err := authenticator.RefreshToken() 65 | assert.NoError(t, err) 66 | assert.Equal(t, []byte("token"), token) 67 | 68 | t.Cleanup(func() { 69 | os.Remove(k8sJWTPath) 70 | }) 71 | }) 72 | 73 | t.Run("Returns error when Authenticate fails", func(t *testing.T) { 74 | authenticator := JWTAuthenticator{ 75 | Authenticate: func(jwt, hostid string) ([]byte, error) { 76 | return nil, assert.AnError 77 | }, 78 | } 79 | 80 | token, err := authenticator.RefreshToken() 81 | assert.Error(t, err) 82 | assert.Nil(t, token) 83 | }) 84 | 85 | t.Run("Returns error when no JWT provided", func(t *testing.T) { 86 | authenticator := JWTAuthenticator{ 87 | Authenticate: func(jwt, hostid string) ([]byte, error) { 88 | return nil, nil 89 | }, 90 | } 91 | 92 | token, err := authenticator.RefreshToken() 93 | assert.ErrorContains(t, err, "Failed to refresh JWT") 94 | assert.Nil(t, token) 95 | }) 96 | } 97 | 98 | func TestJWTAuthenticator_NeedsTokenRefresh(t *testing.T) { 99 | t.Run("Returns false", func(t *testing.T) { 100 | // Test that the NeedsTokenRefresh method always returns false 101 | authenticator := JWTAuthenticator{} 102 | 103 | assert.False(t, authenticator.NeedsTokenRefresh()) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /conjurapi/authn_iam_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var authnIamPolicy = ` 13 | - !policy 14 | id: prod 15 | body: 16 | - !webservice 17 | 18 | - !group clients 19 | 20 | - !permit 21 | role: !group clients 22 | privilege: [ read, authenticate ] 23 | resource: !webservice 24 | 25 | # Give the host permission to authenticate using the IAM Authenticator 26 | - !grant 27 | role: !group clients 28 | member: !host /data/test/myspace/601277729239/InstanceReadJenkinsExecutorHostFactoryToken 29 | ` 30 | var authIamRolesPolicy = ` 31 | - !policy 32 | id: myspace 33 | body: 34 | - &variables 35 | - !variable database/username 36 | - !variable database/password 37 | # Create a group that will have permission to retrieve variables 38 | - !group secrets-users 39 | # Give the secrets-users group permission to retrieve variables 40 | - !permit 41 | role: !group secrets-users 42 | privilege: [ read, execute ] 43 | resource: *variables 44 | 45 | # Create a layer to hold this application's hosts 46 | - !layer 47 | # The host ID needs to match the AWS ARN of the role we wish to authenticate 48 | - !host 601277729239/InstanceReadJenkinsExecutorHostFactoryToken 49 | # Add our host into our layer 50 | - !grant 51 | role: !layer 52 | member: !host 601277729239/InstanceReadJenkinsExecutorHostFactoryToken 53 | # Give the host in our layer permission to retrieve variables 54 | - !grant 55 | member: !layer 56 | role: !group secrets-users 57 | ` 58 | 59 | func TestAuthnIam(t *testing.T) { 60 | // Only run this if running on AWS 61 | if strings.ToLower(os.Getenv("TEST_AWS")) != "true" { 62 | t.Skip("Skipping AWS IAM authn test") 63 | } 64 | 65 | t.Run("authn-iam e2e happy path", func(t *testing.T) { 66 | utils, err := NewTestUtils(&Config{}) 67 | require.NoError(t, err) 68 | 69 | err = utils.SetupWithAuthenticator("iam", authnIamPolicy, authIamRolesPolicy) 70 | require.NoError(t, err) 71 | conjur := utils.Client() 72 | conjur.EnableAuthenticator("iam", "prod", true) 73 | 74 | err = conjur.AddSecret("data/test/myspace/database/username", "secret") 75 | require.NoError(t, err) 76 | err = conjur.AddSecret("data/test/myspace/database/password", "P@ssw0rd!") 77 | require.NoError(t, err) 78 | 79 | // EXERCISE 80 | config := Config{ 81 | ApplianceURL: conjur.config.ApplianceURL, 82 | Account: conjur.config.Account, 83 | AuthnType: "iam", 84 | ServiceID: "prod", 85 | JWTHostID: "data/test/myspace/601277729239/InstanceReadJenkinsExecutorHostFactoryToken", 86 | } 87 | iamConjur, err := NewClientFromAWSCredentials(config) 88 | require.NoError(t, err) 89 | 90 | _, err = iamConjur.GetAuthenticator().RefreshToken() 91 | require.NoError(t, err) 92 | 93 | whoami, err := iamConjur.WhoAmI() 94 | assert.NoError(t, err) 95 | assert.Contains(t, string(whoami), config.JWTHostID) 96 | 97 | secret, err := iamConjur.RetrieveSecret("data/test/myspace/database/username") 98 | assert.NoError(t, err) 99 | assert.Equal(t, "secret", string(secret)) 100 | 101 | secret, err = iamConjur.RetrieveSecret("data/test/myspace/database/password") 102 | assert.NoError(t, err) 103 | assert.Equal(t, "P@ssw0rd!", string(secret)) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /conjurapi/authn/auth_token.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | const ( 11 | TimeFormatToken4 = "2006-01-02 15:04:05 MST" 12 | ) 13 | 14 | // AuthnToken represents a Conjur access token. 15 | // Sample token: 16 | // {"protected":"eyJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiI5M2VjNTEwODRmZTM3Zjc3M2I1ODhlNTYyYWVjZGMxMSJ9","payload":"eyJzdWIiOiJhZG1pbiIsImlhdCI6MTUxMDc1MzI1OX0=","signature":"raCufKOf7sKzciZInQTphu1mBbLhAdIJM72ChLB4m5wKWxFnNz_7LawQ9iYEI_we1-tdZtTXoopn_T1qoTplR9_Bo3KkpI5Hj3DB7SmBpR3CSRTnnEwkJ0_aJ8bql5Cbst4i4rSftyEmUqX-FDOqJdAztdi9BUJyLfbeKTW9OGg-QJQzPX1ucB7IpvTFCEjMoO8KUxZpbHj-KpwqAMZRooG4ULBkxp5nSfs-LN27JupU58oRgIfaWASaDmA98O2x6o88MFpxK_M0FeFGuDKewNGrRc8lCOtTQ9cULA080M5CSnruCqu1Qd52r72KIOAfyzNIiBCLTkblz2fZyEkdSKQmZ8J3AakxQE2jyHmMT-eXjfsEIzEt-IRPJIirI3Qm"} 17 | // https://www.conjur.org/reference/cryptography.html 18 | type AuthnToken struct { 19 | bytes []byte 20 | Protected string `json:"protected"` 21 | Payload string `json:"payload"` 22 | Signature string `json:"signature"` 23 | iat time.Time 24 | exp *time.Time 25 | } 26 | 27 | func hasField(fields map[string]string, name string) (hasField bool) { 28 | _, hasField = fields[name] 29 | return 30 | } 31 | 32 | func NewToken(data []byte) (token *AuthnToken, err error) { 33 | fields := make(map[string]string) 34 | if err = json.Unmarshal(data, &fields); err != nil { 35 | err = fmt.Errorf("Unable to unmarshal token: %s", err) 36 | return 37 | } 38 | 39 | if hasField(fields, "protected") && hasField(fields, "payload") && hasField(fields, "signature") { 40 | t := &AuthnToken{} 41 | token = t 42 | } else { 43 | err = fmt.Errorf("Unrecognized token format") 44 | return 45 | } 46 | 47 | err = token.FromJSON(data) 48 | 49 | return 50 | } 51 | 52 | func (t *AuthnToken) FromJSON(data []byte) (err error) { 53 | t.bytes = data 54 | 55 | err = json.Unmarshal(data, &t) 56 | if err != nil { 57 | err = fmt.Errorf("Unable to unmarshal access token: %s", err) 58 | return 59 | } 60 | 61 | // Example: {"sub":"admin","iat":1510753259} 62 | payloadFields := make(map[string]interface{}) 63 | var payloadJSON []byte 64 | payloadJSON, err = base64.StdEncoding.DecodeString(t.Payload) 65 | if err != nil { 66 | err = fmt.Errorf("access token field 'payload' is not valid base64") 67 | return 68 | } 69 | err = json.Unmarshal(payloadJSON, &payloadFields) 70 | if err != nil { 71 | err = fmt.Errorf("Unable to unmarshal access token field 'payload': %s", err) 72 | return 73 | } 74 | 75 | iat_v, ok := payloadFields["iat"] 76 | if !ok { 77 | err = fmt.Errorf("access token field 'payload' does not contain 'iat'") 78 | return 79 | } 80 | iat_f := iat_v.(float64) 81 | // In the absence of exp, the token expires at iat+8 minutes 82 | t.iat = time.Unix(int64(iat_f), 0) 83 | 84 | exp_v, ok := payloadFields["exp"] 85 | if ok { 86 | exp_f := exp_v.(float64) 87 | exp := time.Unix(int64(exp_f), 0) 88 | t.exp = &exp 89 | if t.iat.After(*t.exp) { 90 | err = fmt.Errorf("access token expired before it was issued") 91 | return 92 | } 93 | } 94 | 95 | return 96 | } 97 | 98 | func (t *AuthnToken) Raw() []byte { 99 | return t.bytes 100 | } 101 | 102 | // ShouldRefresh determines if the token should be refreshed. By default tokens expire 8 minutes after issue. 103 | func (t *AuthnToken) ShouldRefresh() bool { 104 | if t.exp != nil { 105 | // Expire when the token is 85% expired 106 | lifespan := t.exp.Sub(t.iat) 107 | duration := float32(lifespan) * 0.85 108 | return time.Now().After(t.iat.Add(time.Duration(duration))) 109 | } else { 110 | // Token expires 8 minutes after issue, by default 111 | return time.Now().After(t.iat.Add(5 * time.Minute)) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /conjurapi/host_factory.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | 9 | "github.com/cyberark/conjur-api-go/conjurapi/response" 10 | ) 11 | 12 | type HostFactoryTokenResponse struct { 13 | Expiration string `json:"expiration"` 14 | Cidr []string `json:"cidr"` 15 | Token string `json:"token"` 16 | } 17 | 18 | type HostFactoryHostResponse struct { 19 | CreatedAt string `json:"created_at"` 20 | Id string `json:"id"` 21 | Owner string `json:"owner"` 22 | Permissions []string `json:"permissions"` 23 | Annotations []annotation `json:"annotations"` 24 | RestrictedTo []string `json:"restricted_to"` 25 | ApiKey string `json:"api_key"` 26 | } 27 | 28 | type annotation struct { 29 | Name string `json:"name"` 30 | Value string `json:"value"` 31 | } 32 | 33 | func (c *Client) CreateToken(durationStr string, hostFactory string, cidrs []string, count int) ([]HostFactoryTokenResponse, error) { 34 | 35 | data := url.Values{} 36 | duration, err := time.ParseDuration(durationStr) 37 | if err != nil { 38 | return nil, err 39 | } 40 | expiration := time.Now().Add(duration).Format(time.RFC3339) 41 | account, kind, identifier, err := c.parseIDandEnforceKind(hostFactory, "host_factory") 42 | if err != nil { 43 | return nil, err 44 | } 45 | hostFactory = fmt.Sprintf("%s:%s:%s", account, kind, identifier) 46 | data.Set("host_factory", hostFactory) 47 | data.Set("expiration", expiration) 48 | data.Set("count", fmt.Sprint(count)) 49 | for _, cidr := range cidrs { 50 | data.Add("cidr[]", cidr) 51 | } 52 | return c.createToken(data) 53 | } 54 | 55 | func (c *Client) createToken(data url.Values) ([]HostFactoryTokenResponse, error) { 56 | 57 | encodedData := data.Encode() 58 | 59 | req, err := c.CreateTokenRequest(encodedData) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | resp, err := c.SubmitRequest(req) 65 | if err != nil { 66 | return nil, err 67 | } 68 | respData, err := response.DataResponse(resp) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | var jsonResponse []HostFactoryTokenResponse 74 | err = json.Unmarshal(respData, &jsonResponse) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return jsonResponse, response.EmptyResponse(resp) 79 | } 80 | 81 | func (c *Client) DeleteToken(token string) error { 82 | 83 | req, err := c.DeleteTokenRequest(token) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | resp, err := c.SubmitRequest(req) 89 | if err != nil { 90 | return err 91 | } 92 | return response.EmptyResponse(resp) 93 | } 94 | 95 | func (c *Client) CreateHost(id string, token string) (HostFactoryHostResponse, error) { 96 | return c.CreateHostWithAnnotations(id, token, nil) 97 | } 98 | 99 | // CreateHostWithAnnotations creates a new host given a Host ID, HostFactory token, and a map of annotations 100 | func (c *Client) CreateHostWithAnnotations(id string, token string, annotations map[string]string) (HostFactoryHostResponse, error) { 101 | data := url.Values{} 102 | data.Set("id", id) 103 | for name, val := range annotations { 104 | data.Add(fmt.Sprintf("annotations[%s]", name), val) 105 | } 106 | 107 | return c.createHost(data, token) 108 | } 109 | 110 | func (c *Client) createHost(data url.Values, token string) (HostFactoryHostResponse, error) { 111 | 112 | var jsonResponse HostFactoryHostResponse 113 | encodedData := data.Encode() 114 | req, err := c.CreateHostRequest(encodedData, token) 115 | if err != nil { 116 | return jsonResponse, err 117 | } 118 | 119 | resp, err := c.submitRequestWithCustomAuth(req) 120 | if err != nil { 121 | return jsonResponse, err 122 | } 123 | err = response.JSONResponse(resp, &jsonResponse) 124 | return jsonResponse, err 125 | } 126 | -------------------------------------------------------------------------------- /conjurapi/requests_v2.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | const v2APIHeaderBeta string = "application/x.secretsmgr.v2beta+json" 11 | const v2APIHeader string = "application/x.secretsmgr.v2+json" 12 | const v2APIOutgoingHeaderID string = "Accept" 13 | const v2APIIncomingHeaderID string = "Content-Type" 14 | 15 | func (c *ClientV2) CreateAuthenticatorRequest(authenticator *AuthenticatorBase) (*http.Request, error) { 16 | body, err := json.Marshal(authenticator) 17 | 18 | if err != nil { 19 | return nil, fmt.Errorf("failed to marshal authenticator request: %w", err) 20 | } 21 | 22 | request, err := http.NewRequest( 23 | http.MethodPost, 24 | c.authenticatorsURL("", ""), 25 | bytes.NewReader(body), 26 | ) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | request.Header.Add("Content-Type", "application/json") 32 | request.Header.Add(v2APIOutgoingHeaderID, v2APIHeaderBeta) 33 | 34 | return request, nil 35 | } 36 | 37 | func (c *ClientV2) GetAuthenticatorRequest(authenticatorType string, serviceID string) (*http.Request, error) { 38 | request, err := http.NewRequest( 39 | http.MethodGet, 40 | c.authenticatorsURL(authenticatorType, serviceID), 41 | nil, 42 | ) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | request.Header.Add(v2APIOutgoingHeaderID, v2APIHeaderBeta) 48 | 49 | return request, nil 50 | } 51 | 52 | func (c *ClientV2) UpdateAuthenticatorRequest(authenticatorType string, serviceID string, enabled bool) (*http.Request, error) { 53 | body, err := json.Marshal(map[string]bool{"enabled": enabled}) 54 | if err != nil { 55 | return nil, fmt.Errorf("failed to marshal authenticator update request: %w", err) 56 | } 57 | 58 | request, err := http.NewRequest( 59 | http.MethodPatch, 60 | c.authenticatorsURL(authenticatorType, serviceID), 61 | bytes.NewReader(body), 62 | ) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | request.Header.Add(v2APIOutgoingHeaderID, v2APIHeaderBeta) 68 | request.Header.Add("Content-Type", "application/json") 69 | return request, nil 70 | } 71 | 72 | func (c *ClientV2) DeleteAuthenticatorRequest(authenticatorType string, serviceID string) (*http.Request, error) { 73 | request, err := http.NewRequest( 74 | http.MethodDelete, 75 | c.authenticatorsURL(authenticatorType, serviceID), 76 | nil, 77 | ) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | request.Header.Add(v2APIOutgoingHeaderID, v2APIHeaderBeta) 83 | 84 | return request, nil 85 | } 86 | 87 | func (c *ClientV2) ListAuthenticatorsRequest() (*http.Request, error) { 88 | request, err := http.NewRequest( 89 | http.MethodGet, 90 | c.authenticatorsURL("", ""), 91 | nil, 92 | ) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | request.Header.Add(v2APIOutgoingHeaderID, v2APIHeaderBeta) 98 | 99 | return request, nil 100 | } 101 | 102 | func (c *ClientV2) authenticatorsURL(authenticatorType string, serviceID string) string { 103 | // If running against Secrets Manager SaaS, the account is not used in the URL. 104 | account := c.config.Account 105 | if isConjurCloudURL(c.config.ApplianceURL) { 106 | account = "" 107 | } 108 | 109 | // TODO: validate GCP does not use service IDs and if it should be accessible via this API 110 | if authenticatorType == "gcp" { 111 | return makeRouterURL(c.config.ApplianceURL, "authenticators", account, authenticatorType).String() 112 | } 113 | 114 | if authenticatorType != "" && authenticatorType != "authn" { 115 | return makeRouterURL(c.config.ApplianceURL, "authenticators", account, authenticatorType, serviceID).String() 116 | } 117 | 118 | // For the default authenticators service endpoint 119 | return makeRouterURL(c.config.ApplianceURL, "authenticators", account).String() 120 | } 121 | -------------------------------------------------------------------------------- /conjurapi/response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/cyberark/conjur-api-go/conjurapi/logging" 9 | ) 10 | 11 | func readBody(resp *http.Response) ([]byte, error) { 12 | defer resp.Body.Close() 13 | 14 | responseText, err := io.ReadAll(resp.Body) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return responseText, err 20 | } 21 | 22 | func logResponse(resp *http.Response) { 23 | req := resp.Request 24 | redactedHeaders := redactHeaders(req.Header) 25 | logging.ApiLog.Debugf("%d %s %s %+v", resp.StatusCode, req.Method, req.URL, redactedHeaders) 26 | } 27 | 28 | const redactedString = "[REDACTED]" 29 | 30 | // redactHeaders purges Authorization headers, and returns a function to restore them. 31 | func redactHeaders(headers http.Header) http.Header { 32 | origAuthz := headers.Get("Authorization") 33 | if origAuthz != "" { 34 | newHeaders := headers.Clone() 35 | newHeaders.Set("Authorization", redactedString) 36 | return newHeaders 37 | } 38 | return headers 39 | } 40 | 41 | // DataResponse checks the HTTP status of the response. If it's less than 42 | // 300, it returns the response body as a byte array. Otherwise it returns 43 | // a NewConjurError. 44 | func DataResponse(resp *http.Response) ([]byte, error) { 45 | logResponse(resp) 46 | if resp.StatusCode < 300 { 47 | return readBody(resp) 48 | } 49 | return nil, NewConjurError(resp) 50 | } 51 | 52 | // SecretDataResponse checks the HTTP status of the response. If it's less than 53 | // 300, it returns the response body as a stream. Otherwise it returns 54 | // a NewConjurError. 55 | func SecretDataResponse(resp *http.Response) (io.ReadCloser, error) { 56 | logResponse(resp) 57 | if resp.StatusCode < 300 { 58 | return resp.Body, nil 59 | } 60 | return nil, NewConjurError(resp) 61 | } 62 | 63 | // JSONResponse checks the HTTP status of the response. If it's less than 64 | // 300, it returns the response body as JSON. Otherwise it returns 65 | // a NewConjurError. 66 | func JSONResponse(resp *http.Response, obj interface{}) error { 67 | logResponse(resp) 68 | if resp.StatusCode < 300 { 69 | body, err := readBody(resp) 70 | if err != nil { 71 | return err 72 | } 73 | return json.Unmarshal(body, obj) 74 | } 75 | return NewConjurError(resp) 76 | } 77 | 78 | // JSONResponseWithAllowedStatusCodes checks the HTTP status of the response. If it's less than 79 | // 300 or equal to one of the provided values, it returns the response body as JSON. Otherwise it 80 | // returns a NewConjurError. 81 | func JSONResponseWithAllowedStatusCodes(resp *http.Response, obj interface{}, allowedStatusCodes []int) error { 82 | logResponse(resp) 83 | if resp.StatusCode < 300 || contains(allowedStatusCodes, resp.StatusCode) { 84 | body, err := readBody(resp) 85 | if err != nil { 86 | return err 87 | } 88 | return json.Unmarshal(body, obj) 89 | } 90 | return NewConjurError(resp) 91 | } 92 | 93 | func contains(allowedStatusCodes []int, i int) bool { 94 | for _, v := range allowedStatusCodes { 95 | if v == i { 96 | return true 97 | } 98 | } 99 | return false 100 | } 101 | 102 | // EmptyResponse checks the HTTP status of the response. If it's less than 103 | // 300, it returns without an error. Otherwise it returns 104 | // a NewConjurError. 105 | func EmptyResponse(resp *http.Response) error { 106 | logResponse(resp) 107 | if resp.StatusCode < 300 { 108 | return nil 109 | } 110 | return NewConjurError(resp) 111 | } 112 | 113 | // DryRunPolicyJSONResponse checks the HTTP status of the response. If it's less than 114 | // 300 or equal to 422, it returns the response body as JSON. Otherwise it 115 | // returns a NewConjurError. 116 | func DryRunPolicyJSONResponse(resp *http.Response, obj interface{}) error { 117 | return JSONResponseWithAllowedStatusCodes(resp, obj, []int{422}) 118 | } 119 | -------------------------------------------------------------------------------- /conjurapi/authn/gcp_authenticator.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/cyberark/conjur-api-go/conjurapi/logging" 10 | ) 11 | 12 | const GcpMetadataFlavorHeaderName = "Metadata-Flavor" 13 | const GcpMetadataFlavorHeaderValue = "Google" 14 | const GcpIdentityURL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity" 15 | 16 | // GCPAuthenticator handles authentication to Conjur using the authn-gcp authenticator. 17 | // It can either be provided a JWT token directly, or it can fetch a token from the GCP Metadata service. 18 | // It requires the Conjur account name and host ID. It can optionally override the default GCP identity URL. 19 | type GCPAuthenticator struct { 20 | // The Conjur account name. 21 | Account string 22 | // The JWT token from GCP. If empty, a token will be fetched from the GCP Metadata service. 23 | JWT string 24 | // The HostID to use for authentication to Conjur. 25 | HostID string 26 | // The GCP Metadata service URL to fetch the identity token from. Defaults to the standard GCP metadata URL. 27 | GCPIdentityUrl string 28 | // Authenticate is a function that takes a GCP JWT token and returns a Conjur access token or an error. 29 | // It will usually be set to Client.GCPAuthenticate. 30 | Authenticate func(gcpToken string) ([]byte, error) 31 | } 32 | 33 | // RefreshToken fetches a new JWT token from the GCP Metadata service if needed, then uses it to authenticate to Conjur and get a new access token. 34 | func (a *GCPAuthenticator) RefreshToken() ([]byte, error) { 35 | err := a.RefreshJWT() 36 | if err != nil { 37 | return nil, err 38 | } 39 | return a.Authenticate(a.JWT) 40 | } 41 | 42 | func (a *GCPAuthenticator) NeedsTokenRefresh() bool { 43 | return false 44 | } 45 | 46 | // RefreshJWT fetches a new JWT token from the GCP Metadata service if none is set. 47 | func (a *GCPAuthenticator) RefreshJWT() error { 48 | // If a JWT is explicitly set, use it. 49 | if a.JWT != "" { 50 | logging.ApiLog.Debug("Using explicitly set GCP token") 51 | return nil 52 | } 53 | 54 | logging.ApiLog.Debug("No token set, fetching new token") 55 | token, err := a.GCPAuthenticateToken() 56 | if err != nil { 57 | return fmt.Errorf("Failed to refresh GCP token: %v", err) 58 | } 59 | a.JWT = token 60 | logging.ApiLog.Debug("Successfully fetched new token") 61 | 62 | return nil 63 | } 64 | 65 | // GCPAuthenticateToken fetches a GCP token from the GCP Metadata service. 66 | func (a *GCPAuthenticator) GCPAuthenticateToken() (string, error) { 67 | // Build query parameters 68 | params := url.Values{} 69 | audience := "conjur/" + a.Account + "/host/" + a.HostID 70 | params.Add("audience", audience) 71 | params.Add("format", "full") 72 | 73 | // Build final URL with encoded parameters 74 | fullURL := fmt.Sprintf("%s?%s", a.GCPIdentityUrl, params.Encode()) 75 | // Create a new request 76 | req, err := http.NewRequest("GET", fullURL, nil) 77 | if err != nil { 78 | logging.ApiLog.Fatalf("Failed to create request for GCP metadata token: %v", err) 79 | return "", err 80 | } 81 | 82 | // Set required header 83 | req.Header.Add(GcpMetadataFlavorHeaderName, GcpMetadataFlavorHeaderValue) 84 | 85 | // Perform the request 86 | client := &http.Client{} 87 | resp, err := client.Do(req) 88 | if err != nil { 89 | logging.ApiLog.Fatalf("Request failed for GCP Metadata token: %v", err) 90 | return "", err 91 | } 92 | defer resp.Body.Close() 93 | 94 | // Check if response status is not 200 (OK) 95 | if resp.StatusCode != http.StatusOK { 96 | return "", fmt.Errorf("received non-200 response: %v", resp.Status) 97 | } 98 | 99 | // Read the response 100 | body, err := io.ReadAll(resp.Body) 101 | if err != nil { 102 | logging.ApiLog.Fatalf("Failed to read response for GCP metadata token: %v", err) 103 | return "", err 104 | } 105 | 106 | return string(body), nil 107 | } 108 | -------------------------------------------------------------------------------- /conjurapi/group_membership_v2_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var emptyGroupPolicy = ` 11 | - !host bob 12 | - !group test-users 13 | ` 14 | 15 | var hostInGroupPolicy = ` 16 | - !host bob 17 | - !group test-users 18 | 19 | - !grant 20 | role: !group test-users 21 | members: 22 | - !host bob 23 | ` 24 | 25 | func TestClientV2_AddGroupMember(t *testing.T) { 26 | utils, err := NewTestUtils(&Config{}) 27 | require.NoError(t, err) 28 | _, err = utils.Setup(emptyGroupPolicy) 29 | require.NoError(t, err) 30 | conjur := utils.Client().V2() 31 | 32 | testCases := []struct { 33 | name string 34 | groupID string 35 | member GroupMember 36 | expectError string 37 | }{ 38 | { 39 | name: "Add valid host member", 40 | groupID: "data/test/test-users", 41 | member: GroupMember{ID: "data/test/bob", Kind: "host"}, 42 | }, 43 | { 44 | name: "Missing group ID", 45 | groupID: "", 46 | member: GroupMember{ID: "workload@example.com", Kind: "host"}, 47 | expectError: "Must specify a Group ID", 48 | }, 49 | { 50 | name: "Missing member ID", 51 | groupID: "data/test/test-users", 52 | member: GroupMember{ID: "", Kind: "host"}, 53 | expectError: "Must specify a Member", 54 | }, 55 | { 56 | name: "Invalid member kind", 57 | groupID: "data/test/test-users", 58 | member: GroupMember{ID: "workload@example.com", Kind: "invalid"}, 59 | expectError: "Invalid member kind: invalid", 60 | }, 61 | } 62 | 63 | for _, tc := range testCases { 64 | t.Run(tc.name, func(t *testing.T) { 65 | member, err := conjur.AddGroupMember(tc.groupID, tc.member) 66 | if tc.expectError != "" { 67 | assert.Error(t, err) 68 | assert.Contains(t, err.Error(), tc.expectError) 69 | return 70 | } 71 | require.NoError(t, err) 72 | require.NotNil(t, member) 73 | assert.Equal(t, tc.member.ID, member.ID) 74 | expectedPublic := toPublicKind(tc.member.Kind) 75 | assert.True(t, 76 | member.Kind == expectedPublic || member.Kind == tc.member.Kind, 77 | "Unexpected member kind: %s", member.Kind, 78 | ) 79 | }) 80 | } 81 | } 82 | 83 | func TestClientV2_RemoveGroupMember(t *testing.T) { 84 | utils, err := NewTestUtils(&Config{}) 85 | require.NoError(t, err) 86 | _, err = utils.Setup(hostInGroupPolicy) 87 | require.NoError(t, err) 88 | conjur := utils.Client().V2() 89 | 90 | testCases := []struct { 91 | name string 92 | groupID string 93 | member GroupMember 94 | expectError string 95 | }{ 96 | { 97 | name: "Remove valid host member", 98 | groupID: "data/test/test-users", 99 | member: GroupMember{ID: "data/test/bob", Kind: "host"}, 100 | }, 101 | { 102 | name: "Missing group ID", 103 | groupID: "", 104 | member: GroupMember{ID: "workload@example.com", Kind: "host"}, 105 | expectError: "Must specify a Group ID", 106 | }, 107 | { 108 | name: "Missing member ID", 109 | groupID: "data/test/test-users", 110 | member: GroupMember{ID: "", Kind: "host"}, 111 | expectError: "Must specify a Member", 112 | }, 113 | { 114 | name: "Invalid member kind", 115 | groupID: "data/test/test-users", 116 | member: GroupMember{ID: "workload@example.com", Kind: "invalid"}, 117 | expectError: "Invalid member kind: invalid", 118 | }, 119 | } 120 | 121 | for _, tc := range testCases { 122 | t.Run(tc.name, func(t *testing.T) { 123 | _, err := conjur.RemoveGroupMember(tc.groupID, tc.member) 124 | if tc.expectError != "" { 125 | assert.Error(t, err) 126 | assert.Contains(t, err.Error(), tc.expectError) 127 | return 128 | } 129 | require.NoError(t, err) 130 | }) 131 | } 132 | } 133 | 134 | func toPublicKind(kind string) string { 135 | if kind == "host" { 136 | return "workload" 137 | } 138 | return kind 139 | } 140 | -------------------------------------------------------------------------------- /conjurapi/workload_v2.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | 12 | "github.com/cyberark/conjur-api-go/conjurapi/response" 13 | ) 14 | 15 | type AuthnDescriptorData struct { 16 | Claims map[string]string `json:"claims,omitempty"` 17 | } 18 | 19 | type AuthnDescriptor struct { 20 | Type string `json:"type"` 21 | ServiceID string `json:"service_id,omitempty"` 22 | Data *AuthnDescriptorData `json:"data,omitempty"` 23 | } 24 | 25 | type Workload struct { 26 | Name string `json:"name"` 27 | Branch string `json:"branch"` 28 | Type string `json:"type,omitempty"` 29 | Owner *Owner `json:"owner,omitempty"` 30 | Annotations map[string]string `json:"annotations,omitempty"` 31 | AuthnDescriptors []AuthnDescriptor `json:"authn_descriptors"` 32 | RestrictedTo []string `json:"restricted_to,omitempty"` 33 | } 34 | 35 | func (c *ClientV2) CreateWorkload(workload Workload) ([]byte, error) { 36 | if !isConjurCloudURL(c.config.ApplianceURL) { 37 | return nil, fmt.Errorf(NotSupportedInConjurEnterprise, "Workload API") 38 | } 39 | 40 | req, err := c.CreateWorkloadRequest(workload) 41 | if err != nil { 42 | return nil, err 43 | } 44 | resp, err := c.SubmitRequest(req) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return response.DataResponse(resp) 50 | } 51 | 52 | func (c *ClientV2) DeleteWorkload(workloadId string) ([]byte, error) { 53 | if !isConjurCloudURL(c.config.ApplianceURL) { 54 | return nil, fmt.Errorf(NotSupportedInConjurEnterprise, "Workload API") 55 | } 56 | 57 | req, err := c.DeleteWorkloadRequest(workloadId) 58 | if err != nil { 59 | return nil, err 60 | } 61 | resp, err := c.SubmitRequest(req) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return response.DataResponse(resp) 67 | } 68 | 69 | func (c *ClientV2) CreateWorkloadRequest(workload Workload) (*http.Request, error) { 70 | errors := []string{} 71 | 72 | err := workload.Validate() 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | if len(workload.AuthnDescriptors) == 0 { 78 | errors = append(errors, "Must specify at least one authenticator in authn_descriptors") 79 | } else { 80 | for i, d := range workload.AuthnDescriptors { 81 | if d.Type == "" { 82 | errors = append(errors, fmt.Sprintf("authn_descriptors[%d] missing type", i)) 83 | } 84 | } 85 | } 86 | 87 | if len(errors) > 0 { 88 | return nil, fmt.Errorf("%s", strings.Join(errors, " -- ")) 89 | } 90 | // Default type 91 | if workload.Type == "" { 92 | workload.Type = "other" 93 | } 94 | 95 | payload, err := json.Marshal(workload) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | fullURL := makeRouterURL(c.config.ApplianceURL, "workloads").String() 101 | 102 | req, err := http.NewRequest(http.MethodPost, fullURL, bytes.NewBuffer(payload)) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | req.Header.Add("Content-Type", "application/json") 108 | req.Header.Add(v2APIOutgoingHeaderID, v2APIHeaderBeta) 109 | return req, nil 110 | } 111 | 112 | func (c *ClientV2) DeleteWorkloadRequest(workloadID string) (*http.Request, error) { 113 | if workloadID == "" { 114 | return nil, fmt.Errorf("Must specify a Workload ID") 115 | } 116 | 117 | fullURL := makeRouterURL(c.config.ApplianceURL, "hosts", url.QueryEscape(workloadID)).String() 118 | req, err := http.NewRequest(http.MethodDelete, fullURL, nil) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | req.Header.Add(v2APIOutgoingHeaderID, v2APIHeader) 124 | return req, nil 125 | } 126 | 127 | func (w Workload) Validate() error { 128 | var errs []error 129 | if w.Branch == "" { 130 | errs = append(errs, fmt.Errorf("Missing required attribute Workload Branch")) 131 | } 132 | if w.Name == "" { 133 | errs = append(errs, fmt.Errorf("Missing required attribute Workload Name")) 134 | } 135 | if len(errs) > 0 { 136 | return errors.Join(errs...) 137 | } 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /conjurapi/authn/azure_authenticator.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/cyberark/conjur-api-go/conjurapi/logging" 11 | ) 12 | 13 | // AzureAuthenticator handles authentication to Conjur using the authn-azure authenticator. 14 | // It can either be provided a JWT token directly, or it can fetch a token from the Azure Instance Metadata Service (IMDS). 15 | // It can optionally use a specific ClientID to request a token from IMDS. 16 | type AzureAuthenticator struct { 17 | // The JWT token from Azure. If empty, a token will be fetched from IMDS. 18 | JWT string 19 | // Optional ClientID to use when fetching a token from IMDS. 20 | ClientID string 21 | // Authenticate is a function that takes an Azure JWT token and returns a Conjur access token or an error. 22 | // It will usually be set to Client.AzureAuthenticate. 23 | Authenticate func(azureToken string) ([]byte, error) 24 | } 25 | 26 | // RefreshToken fetches a new JWT token from IMDS if needed, then uses it to authenticate to Conjur and get a new access token. 27 | func (a *AzureAuthenticator) RefreshToken() ([]byte, error) { 28 | err := a.RefreshJWT() 29 | if err != nil { 30 | return nil, err 31 | } 32 | return a.Authenticate(a.JWT) 33 | } 34 | 35 | // RefreshJWT fetches a new JWT token from IMDS if none is set. 36 | func (a *AzureAuthenticator) RefreshJWT() error { 37 | // If a JWT is explicitly set, use it. 38 | if a.JWT != "" { 39 | logging.ApiLog.Debug("Using explicitly set Azure token") 40 | return nil 41 | } 42 | 43 | logging.ApiLog.Debug("No token set, fetching new token") 44 | token, err := a.AzureAuthenticateToken() 45 | if err != nil { 46 | return fmt.Errorf("Failed to refresh Azure token: %v", err) 47 | } 48 | a.JWT = token 49 | logging.ApiLog.Debug("Successfully fetched new token") 50 | 51 | return nil 52 | } 53 | 54 | func (a *AzureAuthenticator) NeedsTokenRefresh() bool { 55 | return false 56 | } 57 | 58 | type AzureResponse struct { 59 | AccessToken string `json:"access_token"` 60 | } 61 | 62 | // AzureAuthenticateToken fetches an Azure token from the Azure Instance Metadata Service (IMDS). 63 | func (a *AzureAuthenticator) AzureAuthenticateToken() (string, error) { 64 | req, err := a.AzureTokenRequest() 65 | if err != nil { 66 | return "", err 67 | } 68 | 69 | // Call managed services for Azure resources token endpoint 70 | resp, err := http.DefaultClient.Do(req) 71 | if err != nil { 72 | logging.ApiLog.Errorf("Error calling Azure token endpoint: %v", err) 73 | return "", err 74 | } 75 | 76 | defer resp.Body.Close() 77 | 78 | if resp.StatusCode != http.StatusOK { 79 | return "", fmt.Errorf("Non-OK HTTP status: %s", resp.Status) 80 | } 81 | // Read response body 82 | responseBytes, err := io.ReadAll(resp.Body) 83 | if err != nil { 84 | logging.ApiLog.Errorf("Error reading the response body for Azure token: %v", err) 85 | return "", err 86 | } 87 | 88 | // Unmarshall response body into struct 89 | var r AzureResponse 90 | err = json.Unmarshal(responseBytes, &r) 91 | if err != nil { 92 | logging.ApiLog.Errorf("Error unmarshalling the response for Azure token: %v", err) 93 | return "", err 94 | } 95 | 96 | return r.AccessToken, nil 97 | } 98 | 99 | // Create HTTP request for a managed services for Azure resources token to access Azure Resource Manager 100 | func (a *AzureAuthenticator) AzureTokenRequest() (*http.Request, error) { 101 | azureBaseURL := "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01" 102 | msi_endpoint, err := url.Parse(azureBaseURL) 103 | if err != nil { 104 | return nil, err 105 | } 106 | msi_parameters := msi_endpoint.Query() 107 | if a.ClientID != "" { 108 | msi_parameters.Add("client_id", a.ClientID) 109 | } 110 | msi_parameters.Add("resource", "https://management.azure.com/") 111 | msi_endpoint.RawQuery = msi_parameters.Encode() 112 | req, err := http.NewRequest("GET", msi_endpoint.String(), nil) 113 | if err != nil { 114 | logging.ApiLog.Errorf("Error creating HTTP request: %v", err) 115 | return nil, err 116 | } 117 | req.Header.Add("Metadata", "true") 118 | 119 | return req, nil 120 | } 121 | -------------------------------------------------------------------------------- /conjurapi/group_membership_v2.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/cyberark/conjur-api-go/conjurapi/response" 11 | ) 12 | 13 | type GroupMember struct { 14 | ID string `json:"id"` 15 | Kind string `json:"kind"` 16 | } 17 | 18 | func (c *ClientV2) AddGroupMember(groupID string, member GroupMember) (*GroupMember, error) { 19 | memberResp := GroupMember{} 20 | 21 | if !isConjurCloudURL(c.config.ApplianceURL) && c.VerifyMinServerVersion(MinVersion) != nil { 22 | return nil, fmt.Errorf(NotSupportedInOldVersions, "Group Membership API", MinVersion) 23 | } 24 | 25 | req, err := c.AddGroupMemberRequest(groupID, member) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | resp, err := c.SubmitRequest(req) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return &memberResp, response.JSONResponse(resp, &memberResp) 36 | } 37 | 38 | func (c *ClientV2) RemoveGroupMember(groupID string, member GroupMember) ([]byte, error) { 39 | if !isConjurCloudURL(c.config.ApplianceURL) && c.VerifyMinServerVersion(MinVersion) != nil { 40 | return nil, fmt.Errorf(NotSupportedInOldVersions, "Group Membership API", MinVersion) 41 | } 42 | 43 | req, err := c.RemoveGroupMemberRequest(groupID, member) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | resp, err := c.SubmitRequest(req) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return response.DataResponse(resp) 54 | } 55 | 56 | func (c *ClientV2) AddGroupMemberRequest(groupID string, member GroupMember) (*http.Request, error) { 57 | if groupID == "" { 58 | return nil, fmt.Errorf("Must specify a Group ID") 59 | } 60 | 61 | err := member.Validate() 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | payload, err := json.Marshal(member) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | req, err := http.NewRequest(http.MethodPost, c.addGroupMembershipURL(groupID), bytes.NewBuffer(payload)) 72 | if err != nil { 73 | return nil, fmt.Errorf("Failed to create add group member request: %w", err) 74 | } 75 | 76 | req.Header.Add(v2APIOutgoingHeaderID, v2APIHeader) 77 | req.Header.Add(v2APIIncomingHeaderID, "application/json") 78 | 79 | if !isConjurCloudURL(c.config.ApplianceURL) { 80 | req.Header.Add(v2APIOutgoingHeaderID, v2APIHeaderBeta) 81 | } 82 | return req, nil 83 | } 84 | 85 | func (c *ClientV2) RemoveGroupMemberRequest(groupID string, member GroupMember) (*http.Request, error) { 86 | if groupID == "" { 87 | return nil, fmt.Errorf("Must specify a Group ID") 88 | } 89 | err := member.Validate() 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | req, err := http.NewRequest(http.MethodDelete, c.removeGroupMembershipURL(groupID, member), nil) 95 | if err != nil { 96 | return nil, fmt.Errorf("Failed to create remove group member request: %v", err) 97 | } 98 | req.Header.Add(v2APIOutgoingHeaderID, v2APIHeader) 99 | if !isConjurCloudURL(c.config.ApplianceURL) { 100 | req.Header.Add(v2APIOutgoingHeaderID, v2APIHeaderBeta) 101 | } 102 | return req, nil 103 | } 104 | 105 | func (member GroupMember) Validate() error { 106 | var errs []error 107 | if member.ID == "" || member.Kind == "" { 108 | errs = append(errs, fmt.Errorf("Must specify a Member")) 109 | } 110 | 111 | switch member.Kind { 112 | case "user", "host", "group": 113 | default: 114 | errs = append(errs, fmt.Errorf("Invalid member kind: %v", member.Kind)) 115 | } 116 | 117 | if len(errs) > 0 { 118 | return errors.Join(errs...) 119 | } 120 | return nil 121 | } 122 | 123 | func (c *ClientV2) addGroupMembershipURL(groupID string) string { 124 | account := c.config.Account 125 | if isConjurCloudURL(c.config.ApplianceURL) { 126 | account = "" 127 | } 128 | return makeRouterURL(c.config.ApplianceURL, "groups", account, groupID, "members").String() 129 | } 130 | 131 | func (c *ClientV2) removeGroupMembershipURL(groupID string, member GroupMember) string { 132 | account := c.config.Account 133 | if isConjurCloudURL(c.config.ApplianceURL) { 134 | account = "" 135 | } 136 | return makeRouterURL(c.config.ApplianceURL, "groups", account, groupID, "members", member.Kind, member.ID).String() 137 | } 138 | -------------------------------------------------------------------------------- /conjurapi/authn/iam_authenticator.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "regexp" 9 | "strings" 10 | "time" 11 | 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" 14 | awsconfig "github.com/aws/aws-sdk-go-v2/config" 15 | 16 | "github.com/cyberark/conjur-api-go/conjurapi/logging" 17 | ) 18 | 19 | // IAMAuthenticator handles authentication to Conjur using the authn-iam authenticator. 20 | // It uses AWS SDK to sign a request to the AWS STS GetCallerIdentity endpoint and sends the signed headers to Conjur 21 | // to get a Conjur access token. 22 | type IAMAuthenticator struct { 23 | // Authenticate is a function that returns a Conjur access token or an error. 24 | // It will usually be set to Client.IAMAuthenticate. 25 | Authenticate func() ([]byte, error) 26 | } 27 | 28 | // RefreshToken uses the Authenticate function to get a new Conjur access token. 29 | func (a *IAMAuthenticator) RefreshToken() ([]byte, error) { 30 | return a.Authenticate() 31 | } 32 | 33 | func (a *IAMAuthenticator) NeedsTokenRefresh() bool { 34 | return false 35 | } 36 | 37 | // IAMAuthenticateHeaders fetches AWS credentials and signs a request to the AWS STS GetCallerIdentity endpoint. 38 | // These headers can then be sent to Conjur to authenticate using the authn-iam authenticator. 39 | func IAMAuthenticateHeaders() ([]byte, error) { 40 | ctx := context.TODO() 41 | cfg, err := awsconfig.LoadDefaultConfig(ctx) 42 | if err != nil { 43 | logging.ApiLog.Errorf("Error loading AWS config: %v", err) 44 | return nil, err 45 | } 46 | 47 | if cfg.Region == "" { 48 | cfg.Region = "us-east-1" 49 | } 50 | 51 | request, err := buildRequest(cfg) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | creds, err := cfg.Credentials.Retrieve(ctx) 57 | if err != nil { 58 | logging.ApiLog.Errorf("Error loading AWS credentials: %v", err) 59 | return nil, err 60 | } 61 | 62 | // Sign the request 63 | signer := v4.NewSigner() 64 | // NOTE: The random-looking string is a hash of an empty payload which is necessary for the correct signature 65 | err = signer.SignHTTP(ctx, creds, request, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "sts", cfg.Region, time.Now().UTC()) 66 | if err != nil { 67 | logging.ApiLog.Errorf("Error signing HTTP request: %v", err) 68 | return nil, err 69 | } 70 | 71 | headerMap := make(map[string]interface{}) 72 | for key, values := range request.Header { 73 | if len(values) == 1 { 74 | headerMap[key] = values[0] 75 | } 76 | } 77 | 78 | jsonData, err := json.Marshal(headerMap) 79 | if err != nil { 80 | logging.ApiLog.Errorf("Error marshalling signed headers to JSON: %v", err) 81 | return nil, err 82 | } 83 | 84 | return jsonData, nil 85 | } 86 | 87 | func buildRequest(cfg aws.Config) (*http.Request, error) { 88 | if !isValidAWSRegion(cfg.Region) { 89 | return nil, fmt.Errorf("Invalid AWS region: %s", cfg.Region) 90 | } 91 | 92 | var stsEndpoint string 93 | if cfg.Region == "global" { 94 | stsEndpoint = "https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15" 95 | } else { 96 | stsEndpoint = fmt.Sprintf("https://sts.%s.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15", cfg.Region) 97 | } 98 | 99 | request, err := http.NewRequest(http.MethodGet, stsEndpoint, nil) 100 | if err != nil { 101 | logging.ApiLog.Errorf("Error creating HTTP request: %v", err) 102 | return nil, err 103 | } 104 | 105 | if !isValidAWSHost(request.Host) { 106 | return nil, fmt.Errorf("Invalid AWS STS endpoint host: %s", request.Host) 107 | } 108 | 109 | request.Header.Set("Host", request.Host) 110 | return request, nil 111 | } 112 | 113 | func isValidAWSRegion(region string) bool { 114 | // Basic validation for AWS region format. An invalid region could lead to vulnerabilities. 115 | if region == "global" { 116 | return true 117 | } 118 | 119 | return regexp.MustCompile(`^([a-z]{2}(-gov)?-[a-z]+-\d)$`).MatchString(region) 120 | } 121 | 122 | func isValidAWSHost(host string) bool { 123 | // Extra validation to ensure the STS host is not manipulated to point to a non-AWS domain 124 | return strings.HasSuffix(host, ".amazonaws.com") 125 | } 126 | -------------------------------------------------------------------------------- /conjurapi/role_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var roleTestPolicy = ` 11 | - !host bob 12 | - !host jimmy 13 | - !host dean 14 | - !group test-users 15 | - !layer test-layer 16 | 17 | - !variable secret 18 | 19 | - !permit 20 | role: !host bob 21 | privilege: [ execute ] 22 | resource: !variable secret 23 | 24 | - !grant 25 | role: !layer test-layer 26 | members: 27 | - !host jimmy 28 | - !host bob 29 | - !group test-users 30 | 31 | - !grant 32 | role: !group test-users 33 | member: !host dean 34 | ` 35 | 36 | func TestClient_RoleExists(t *testing.T) { 37 | roleExistent := func(conjur *Client, id string) func(t *testing.T) { 38 | return func(t *testing.T) { 39 | exists, err := conjur.RoleExists(id) 40 | assert.NoError(t, err) 41 | assert.True(t, exists) 42 | } 43 | } 44 | 45 | roleNonexistent := func(conjur *Client, id string) func(t *testing.T) { 46 | return func(t *testing.T) { 47 | exists, err := conjur.RoleExists(id) 48 | assert.NoError(t, err) 49 | assert.False(t, exists) 50 | } 51 | } 52 | 53 | roleInvalid := func(conjur *Client, id string) func(t *testing.T) { 54 | return func(t *testing.T) { 55 | exists, err := conjur.RoleExists(id) 56 | assert.Error(t, err) 57 | assert.False(t, exists) 58 | } 59 | } 60 | 61 | utils, err := NewTestUtils(&Config{}) 62 | assert.NoError(t, err) 63 | 64 | _, err = utils.Setup(utils.DefaultTestPolicy()) 65 | assert.NoError(t, err) 66 | conjur := utils.Client() 67 | 68 | t.Run("Role exists returns true", roleExistent(conjur, "conjur:host:data/test/bob")) 69 | t.Run("Role exists returns false", roleNonexistent(conjur, "conjur:user:data/test/nonexistent")) 70 | t.Run("Role exists returns error", roleInvalid(conjur, "")) 71 | } 72 | 73 | func TestClient_Role(t *testing.T) { 74 | showRole := func(conjur *Client, id string) func(t *testing.T) { 75 | return func(t *testing.T) { 76 | _, err := conjur.Role(id) 77 | assert.NoError(t, err) 78 | } 79 | } 80 | 81 | utils, err := NewTestUtils(&Config{}) 82 | assert.NoError(t, err) 83 | 84 | _, err = utils.Setup(roleTestPolicy) 85 | assert.NoError(t, err) 86 | 87 | conjur := utils.Client() 88 | 89 | t.Run("Shows a role", showRole(conjur, "conjur:host:data/test/bob")) 90 | } 91 | 92 | func TestClient_RoleMembers(t *testing.T) { 93 | listMembers := func(conjur *Client, id string, expected int) func(t *testing.T) { 94 | return func(t *testing.T) { 95 | members, err := conjur.RoleMembers(id) 96 | assert.NoError(t, err) 97 | assert.Len(t, members, expected) 98 | } 99 | } 100 | 101 | utils, err := NewTestUtils(&Config{}) 102 | assert.NoError(t, err) 103 | 104 | conjur := utils.Client() 105 | _, err = utils.Setup(roleTestPolicy) 106 | assert.NoError(t, err) 107 | 108 | t.Run("List admin role members return 1 member", listMembers(conjur, fmt.Sprintf("conjur:user:%s", utils.AdminUser()), 1)) 109 | t.Run("List role members return members", listMembers(conjur, "conjur:layer:data/test/test-layer", 4)) 110 | } 111 | 112 | func TestClient_RoleMemberships(t *testing.T) { 113 | testMemberships := func(conjur *Client, id string, expectedDirect, expectedAll int) func(t *testing.T) { 114 | return func(t *testing.T) { 115 | t.Run("Direct memberships only", func(t *testing.T) { 116 | memberships, err := conjur.RoleMemberships(id) 117 | assert.NoError(t, err) 118 | assert.Len(t, memberships, expectedDirect) 119 | }) 120 | 121 | t.Run("All memberships", func(t *testing.T) { 122 | memberships, err := conjur.RoleMembershipsAll(id) 123 | assert.NoError(t, err) 124 | assert.Len(t, memberships, expectedAll) 125 | }) 126 | } 127 | } 128 | 129 | utils, err := NewTestUtils(&Config{}) 130 | assert.NoError(t, err) 131 | 132 | _, err = utils.Setup(roleTestPolicy) 133 | assert.NoError(t, err) 134 | 135 | conjur := utils.Client() 136 | 137 | t.Run("Bob's memberships", testMemberships(conjur, "conjur:host:data/test/bob", 1, 2)) 138 | t.Run("Test layer memberships", testMemberships(conjur, "conjur:layer:data/test/test-layer", 0, 1)) 139 | t.Run("Dean's memberships", testMemberships(conjur, "conjur:host:data/test/dean", 1, 3)) 140 | } 141 | -------------------------------------------------------------------------------- /conjurapi/storage/netrc_storage_provider.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/bgentry/go-netrc/netrc" 10 | ) 11 | 12 | type NetrcStorageProvider struct { 13 | netRCPath string 14 | machineName string 15 | } 16 | 17 | func NewNetrcStorageProvider(netRCPath, machineName string) (*NetrcStorageProvider, error) { 18 | 19 | netrcStorageProvider := &NetrcStorageProvider{ 20 | netRCPath: netRCPath, 21 | machineName: machineName, 22 | } 23 | if netRCPath == "" { 24 | home, err := os.UserHomeDir() 25 | if err != nil { 26 | return nil, fmt.Errorf("Failed to get user home directory: %v", err) 27 | } 28 | netRCPath = filepath.Join(home, ".netrc") 29 | netrcStorageProvider.netRCPath = netRCPath 30 | } 31 | // Ensure file exists 32 | err := netrcStorageProvider.ensureNetrcFileExists() 33 | if err != nil { 34 | return nil, fmt.Errorf("failed to ensure .netrc file exists: %w", err) 35 | } 36 | return netrcStorageProvider, nil 37 | } 38 | 39 | // StoreCredentials stores credentials to the specified .netrc file 40 | func (s *NetrcStorageProvider) StoreCredentials(login string, password string) error { 41 | 42 | nrc, err := netrc.ParseFile(s.netRCPath) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | m := nrc.FindMachine(s.machineName) 48 | if m == nil || m.IsDefault() { 49 | _ = nrc.NewMachine(s.machineName, login, password, "") 50 | } else { 51 | m.UpdateLogin(login) 52 | m.UpdatePassword(password) 53 | } 54 | 55 | data, err := nrc.MarshalText() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | data = ensureEndsWithNewline(data) 61 | 62 | return os.WriteFile(s.netRCPath, data, 0600) 63 | } 64 | 65 | func (s *NetrcStorageProvider) ReadCredentials() (string, string, error) { 66 | nrc, err := netrc.ParseFile(s.netRCPath) 67 | if err != nil { 68 | return "", "", err 69 | } 70 | 71 | m := nrc.FindMachine(s.machineName) 72 | if m == nil { 73 | return "", "", fmt.Errorf(".netrc file was read, but credential for machine %s was not found.", s.machineName) 74 | } 75 | 76 | return m.Login, m.Password, nil 77 | } 78 | 79 | // ReadAuthnToken fetches the cached conjur access token. We only do this for OIDC 80 | // since we don't have access to the Conjur API key and this is the only credential we can save. 81 | func (s *NetrcStorageProvider) ReadAuthnToken() ([]byte, error) { 82 | _, tokenStr, err := s.ReadCredentials() 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return []byte(tokenStr), nil 88 | } 89 | 90 | // StoreAuthnToken stores the conjur access token. We only do this for OIDC 91 | // since we don't have access to the Conjur API key and this is the only credential we can save. 92 | func (s *NetrcStorageProvider) StoreAuthnToken(token []byte) error { 93 | // We should be able to use an empty string for username, but unfortunately 94 | // this causes panics later on. Instead use a dummy value. 95 | return s.StoreCredentials("[oidc]", string(token)) 96 | } 97 | 98 | // PurgeCredentials purges credentials from the specified .netrc file 99 | func (s *NetrcStorageProvider) PurgeCredentials() error { 100 | // Remove cached credentials (username, api key) from .netrc 101 | nrc, err := netrc.ParseFile(s.netRCPath) 102 | if err != nil { 103 | // If the .netrc file doesn't exist, we don't need to do anything 104 | if errors.Is(err, os.ErrNotExist) { 105 | return nil 106 | } 107 | // Any other error should be returned 108 | return err 109 | } 110 | 111 | nrc.RemoveMachine(s.machineName) 112 | 113 | data, err := nrc.MarshalText() 114 | if err != nil { 115 | return err 116 | } 117 | 118 | return os.WriteFile(s.netRCPath, data, 0600) 119 | } 120 | 121 | func (s *NetrcStorageProvider) ensureNetrcFileExists() error { 122 | _, err := os.Stat(s.netRCPath) 123 | if err != nil { 124 | if errors.Is(err, os.ErrNotExist) { 125 | err = os.WriteFile(s.netRCPath, []byte{}, 0600) 126 | if err != nil { 127 | return err 128 | } 129 | } else { 130 | return err 131 | } 132 | } 133 | return nil 134 | } 135 | 136 | func ensureEndsWithNewline(data []byte) []byte { 137 | if data[len(data)-1] != byte('\n') { 138 | data = append(data, byte('\n')) 139 | } 140 | return data 141 | } 142 | -------------------------------------------------------------------------------- /conjurapi/response/error_test.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestNewConjurError(t *testing.T) { 14 | testCases := []struct { 15 | name string 16 | resp *http.Response 17 | expected *ConjurError 18 | }{ 19 | { 20 | name: "simple error", 21 | resp: &http.Response{ 22 | StatusCode: 404, 23 | Status: "Not Found", 24 | Body: io.NopCloser(strings.NewReader(`{"error": {"message": "Not Found"}}`)), 25 | }, 26 | expected: &ConjurError{ 27 | Code: 404, 28 | Message: "Not Found", 29 | Details: &ConjurErrorDetails{ 30 | Message: "Not Found", 31 | }, 32 | }, 33 | }, 34 | { 35 | name: "Conjur error with details", 36 | resp: &http.Response{ 37 | StatusCode: 404, 38 | Status: "Not Found", 39 | Body: io.NopCloser(strings.NewReader(`{"error":{"code":"not_found","message":"CONJ00076E Variable conjur:variable:some_var is empty or not found."}}`)), 40 | }, 41 | expected: &ConjurError{ 42 | Code: 404, 43 | Message: "Not Found", 44 | Details: &ConjurErrorDetails{ 45 | Message: "CONJ00076E Variable conjur:variable:some_var is empty or not found.", 46 | Code: "not_found", 47 | }, 48 | }, 49 | }, 50 | { 51 | name: "empty body", 52 | resp: &http.Response{ 53 | StatusCode: 403, 54 | Status: "Forbidden", 55 | Body: io.NopCloser(strings.NewReader("")), 56 | }, 57 | expected: &ConjurError{ 58 | Code: 403, 59 | Message: "Forbidden", 60 | }, 61 | }, 62 | { 63 | name: "invalid JSON", 64 | resp: &http.Response{ 65 | StatusCode: 403, 66 | Status: "Forbidden", 67 | Body: io.NopCloser(strings.NewReader(`not json`)), 68 | }, 69 | expected: &ConjurError{ 70 | Code: 403, 71 | Message: "Forbidden", 72 | }, 73 | }, 74 | } 75 | 76 | for _, tc := range testCases { 77 | t.Run(tc.name, func(t *testing.T) { 78 | err := NewConjurError(tc.resp) 79 | 80 | require.Error(t, err) 81 | cerr, ok := err.(*ConjurError) 82 | require.True(t, ok, "expected error to be a *ConjurError, got %T", err) 83 | 84 | assert.EqualValues(t, tc.expected.Code, cerr.Code) 85 | }) 86 | } 87 | 88 | t.Run("error reading body", func(t *testing.T) { 89 | resp := &http.Response{ 90 | Body: io.NopCloser(&errorReader{}), 91 | } 92 | 93 | err := NewConjurError(resp) 94 | require.Error(t, err) 95 | assert.EqualError(t, err, "test read error") 96 | }) 97 | } 98 | 99 | func TestConjurError_Error(t *testing.T) { 100 | testCases := []struct { 101 | name string 102 | conjurErr *ConjurError 103 | expected string 104 | }{ 105 | { 106 | name: "with message and details", 107 | conjurErr: &ConjurError{ 108 | Code: 404, 109 | Message: "Not Found", 110 | Details: &ConjurErrorDetails{ 111 | Message: "CONJ00076E Variable conjur:variable:some_var is empty or not found", 112 | Code: "not_found", 113 | }, 114 | }, 115 | expected: "Not Found. CONJ00076E Variable conjur:variable:some_var is empty or not found.", 116 | }, 117 | { 118 | name: "with message only", 119 | conjurErr: &ConjurError{ 120 | Code: 403, 121 | Message: "Forbidden", 122 | }, 123 | expected: "Forbidden", 124 | }, 125 | { 126 | name: "with details only", 127 | conjurErr: &ConjurError{ 128 | Code: 404, 129 | Details: &ConjurErrorDetails{ 130 | Message: "CONJ00076E Variable conjur:variable:some_var is empty or not found", 131 | Code: "not_found", 132 | }, 133 | }, 134 | expected: "CONJ00076E Variable conjur:variable:some_var is empty or not found.", 135 | }, 136 | { 137 | name: "with empty message and details", 138 | conjurErr: &ConjurError{ 139 | Code: 500, 140 | Message: "", 141 | Details: &ConjurErrorDetails{ 142 | Message: "", 143 | Code: "", 144 | }, 145 | }, 146 | expected: "", 147 | }, 148 | } 149 | 150 | for _, tc := range testCases { 151 | t.Run(tc.name, func(t *testing.T) { 152 | actual := tc.conjurErr.Error() 153 | assert.Equal(t, tc.expected, actual) 154 | }) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /conjurapi/storage_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cyberark/conjur-api-go/conjurapi/storage" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/zalando/go-keyring" 9 | ) 10 | 11 | func TestGetMachineName(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | config Config 15 | expected string 16 | }{ 17 | { 18 | name: "default authn", 19 | config: Config{ 20 | ApplianceURL: "https://conjur", 21 | }, 22 | expected: "https://conjur/authn", 23 | }, 24 | { 25 | name: "authn-oidc", 26 | config: Config{ 27 | ApplianceURL: "https://conjur", 28 | AuthnType: "oidc", 29 | ServiceID: "test-service", 30 | }, 31 | expected: "https://conjur/authn-oidc/test-service", 32 | }, 33 | } 34 | for _, tc := range testCases { 35 | t.Run(tc.name, func(t *testing.T) { 36 | machineName := getMachineName(tc.config) 37 | assert.Equal(t, tc.expected, machineName) 38 | }) 39 | } 40 | } 41 | 42 | func TestCreateStorageProvider(t *testing.T) { 43 | testCases := []struct { 44 | name string 45 | config Config 46 | action func() 47 | assert func(t *testing.T, storageProvider CredentialStorageProvider, err error) 48 | }{ 49 | { 50 | name: "default storage", 51 | config: Config{ 52 | ApplianceURL: "https://conjur", 53 | }, 54 | assert: func(t *testing.T, storageProvider CredentialStorageProvider, err error) { 55 | // Keyring shouldn't be avaialble by default in the container running the tests 56 | // Therefore it should default to netrc file storage 57 | assert.Nil(t, err) 58 | assert.NotNil(t, storageProvider) 59 | assert.IsType(t, &storage.NetrcStorageProvider{}, storageProvider) 60 | }, 61 | }, 62 | { 63 | name: "keyring storage when not available", 64 | config: Config{ 65 | ApplianceURL: "https://conjur", 66 | CredentialStorage: "keyring", 67 | }, 68 | assert: func(t *testing.T, storageProvider CredentialStorageProvider, err error) { 69 | assert.ErrorContains(t, err, "Keyring is not available") 70 | }, 71 | }, 72 | { 73 | name: "default storage with keyring available", 74 | config: Config{ 75 | ApplianceURL: "https://conjur", 76 | }, 77 | action: func() { 78 | // Enable a mock memory-based keyring storage 79 | keyring.MockInit() 80 | }, 81 | assert: func(t *testing.T, storageProvider CredentialStorageProvider, err error) { 82 | assert.Nil(t, err) 83 | assert.NotNil(t, storageProvider) 84 | assert.IsType(t, &storage.KeyringStorageProvider{}, storageProvider) 85 | }, 86 | }, 87 | { 88 | name: "keyring storage when available", 89 | config: Config{ 90 | ApplianceURL: "https://conjur", 91 | CredentialStorage: "keyring", 92 | }, 93 | action: func() { 94 | // Enable a mock memory-based keyring storage 95 | keyring.MockInit() 96 | }, 97 | assert: func(t *testing.T, storageProvider CredentialStorageProvider, err error) { 98 | assert.Nil(t, err) 99 | assert.NotNil(t, storageProvider) 100 | assert.IsType(t, &storage.KeyringStorageProvider{}, storageProvider) 101 | }, 102 | }, 103 | { 104 | name: "netrc storage", 105 | config: Config{ 106 | ApplianceURL: "https://conjur", 107 | CredentialStorage: "file", 108 | }, 109 | assert: func(t *testing.T, storageProvider CredentialStorageProvider, err error) { 110 | assert.Nil(t, err) 111 | assert.NotNil(t, storageProvider) 112 | assert.IsType(t, &storage.NetrcStorageProvider{}, storageProvider) 113 | }, 114 | }, 115 | { 116 | name: "no storage", 117 | config: Config{ 118 | ApplianceURL: "https://conjur", 119 | CredentialStorage: "none", 120 | }, 121 | assert: func(t *testing.T, storageProvider CredentialStorageProvider, err error) { 122 | assert.Nil(t, err) 123 | assert.Nil(t, storageProvider) 124 | }, 125 | }, 126 | { 127 | name: "invalid storage option", 128 | config: Config{ 129 | ApplianceURL: "https://conjur", 130 | CredentialStorage: "invalid", 131 | }, 132 | assert: func(t *testing.T, storageProvider CredentialStorageProvider, err error) { 133 | assert.ErrorContains(t, err, "Unknown credential storage type") 134 | }, 135 | }, 136 | } 137 | 138 | for _, tc := range testCases { 139 | t.Run(tc.name, func(t *testing.T) { 140 | if tc.action != nil { 141 | tc.action() 142 | } 143 | 144 | storage, err := createStorageProvider(tc.config) 145 | tc.assert(t, storage, err) 146 | }) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /conjurapi/resource_json_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | const ( 12 | jsonA = ` 13 | { 14 | "identifier": "demo:user:alice", 15 | "id": "alice", 16 | "type": "user", 17 | "owner": "demo:user:admin", 18 | "policy": "demo:user:example", 19 | "annotations": {"key": "value"}, 20 | "permissions": {"execute": ["demo:variable:example/alpha/secret01","demo:variable"]}, 21 | "members": ["demo:user:admin"],"memberships": ["demo:group:secret-users"], 22 | "restricted_to": ["127.0.0.1"] 23 | }` 24 | 25 | jsonB = ` 26 | { 27 | "identifier": "conjur:variable:example/alpha/secret01", 28 | "id": "example/alpha/secret01", 29 | "type": "variable", 30 | "owner": "conjur:policy:example/alpha", 31 | "policy": "conjur:policy:root", 32 | "permitted": { 33 | "execute": [ 34 | "conjur:group:example/alpha/secret-users" 35 | ], 36 | "read": [ 37 | "conjur:group:example/alpha/secret-users" 38 | ] 39 | }, 40 | "annotations": { 41 | "key": "value" 42 | } 43 | }` 44 | ) 45 | 46 | var ( 47 | resourceA = Resource{ 48 | Identifier: "demo:user:alice", 49 | Id: "alice", 50 | Type: "user", 51 | Owner: "demo:user:admin", 52 | Policy: "demo:user:example", 53 | Annotations: map[string]string{"key": "value"}, 54 | Permissions: &map[string][]string{ 55 | "execute": { 56 | "demo:variable:example/alpha/secret01", 57 | "demo:variable", 58 | }, 59 | }, 60 | Members: &[]string{"demo:user:admin"}, 61 | Memberships: &[]string{"demo:group:secret-users"}, 62 | RestrictedTo: &[]string{"127.0.0.1"}, 63 | } 64 | 65 | resourceB = Resource{ 66 | Identifier: "conjur:variable:example/alpha/secret01", 67 | Id: "example/alpha/secret01", 68 | Type: "variable", 69 | Owner: "conjur:policy:example/alpha", 70 | Policy: "conjur:policy:root", 71 | Permitted: &map[string][]string{ 72 | "execute": []string{"conjur:group:example/alpha/secret-users"}, 73 | "read": []string{"conjur:group:example/alpha/secret-users"}, 74 | }, 75 | Annotations: map[string]string{"key": "value"}, 76 | } 77 | resourceList = []Resource{resourceA, resourceB} 78 | ) 79 | 80 | func TestResource_UnmarshalJSON(t *testing.T) { 81 | var unmarshalledResource Resource 82 | tests := []struct { 83 | name string 84 | arg string 85 | want Resource 86 | }{ 87 | { 88 | name: "Unmarshal Role", 89 | arg: jsonA, 90 | want: resourceA, 91 | }, 92 | { 93 | name: "Unmarshal Resource", 94 | arg: jsonB, 95 | want: resourceB, 96 | }, 97 | } 98 | for _, tt := range tests { 99 | t.Run(tt.name, func(t *testing.T) { 100 | json.Unmarshal([]byte(tt.arg), &unmarshalledResource) 101 | assert.Equal(t, &tt.want, &unmarshalledResource) 102 | unmarshalledResource = Resource{} 103 | }) 104 | } 105 | } 106 | 107 | func TestResource_MarshalJSON(t *testing.T) { 108 | tests := []struct { 109 | name string 110 | arg Resource 111 | want string 112 | }{ 113 | { 114 | name: "Marshal Role", 115 | arg: resourceA, 116 | want: jsonA, 117 | }, 118 | { 119 | name: "Marshal Resource", 120 | arg: resourceB, 121 | want: jsonB, 122 | }, 123 | } 124 | for _, tt := range tests { 125 | t.Run(tt.name, func(t *testing.T) { 126 | result, err := json.Marshal(tt.arg) 127 | assert.Nil(t, err) 128 | assert.JSONEq(t, tt.want, string(result)) 129 | }) 130 | } 131 | } 132 | 133 | func TestResources_MarshalJSON(t *testing.T) { 134 | tests := []struct { 135 | name string 136 | arg []Resource 137 | want string 138 | }{ 139 | { 140 | name: "Marshal List", 141 | arg: resourceList, 142 | want: fmt.Sprintf("[%s,%s]", jsonA, jsonB), 143 | }, 144 | } 145 | for _, tt := range tests { 146 | t.Run(tt.name, func(t *testing.T) { 147 | result, err := json.Marshal(tt.arg) 148 | assert.Nil(t, err) 149 | assert.JSONEq(t, tt.want, string(result)) 150 | }) 151 | } 152 | } 153 | func TestResources_UnmarshalJSON(t *testing.T) { 154 | var unmarshalledResources []Resource 155 | tests := []struct { 156 | name string 157 | arg string 158 | want []Resource 159 | }{ 160 | { 161 | name: "Unmarshal List", 162 | arg: fmt.Sprintf("[%s,%s]", jsonA, jsonB), 163 | want: resourceList, 164 | }, 165 | } 166 | for _, tt := range tests { 167 | t.Run(tt.name, func(t *testing.T) { 168 | json.Unmarshal([]byte(tt.arg), &unmarshalledResources) 169 | assert.Equal(t, &tt.want, &unmarshalledResources) 170 | }) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /conjurapi/requests_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUnopinionatedParseID(t *testing.T) { 10 | testCases := []struct { 11 | name string 12 | input string 13 | want []string 14 | }{ 15 | { 16 | name: "simple full id", 17 | input: "account:kind:identifier", 18 | want: []string{"account", "kind", "identifier"}, 19 | }, 20 | { 21 | name: "simple kind and identifier", 22 | input: "kind:identifier", 23 | want: []string{"", "kind", "identifier"}, 24 | }, 25 | { 26 | name: "simple identifier", 27 | input: "identifier", 28 | want: []string{"", "", "identifier"}, 29 | }, 30 | { 31 | name: "empty string", 32 | input: "", 33 | want: []string{"", "", ""}, 34 | }, 35 | { 36 | name: "empty string with colon", 37 | input: "::", 38 | want: []string{"", "", ""}, 39 | }, 40 | { 41 | name: "full id with colon", 42 | input: "account:kind:ident:ifier", 43 | want: []string{"account", "kind", "ident:ifier"}, 44 | }, 45 | { 46 | name: "full id with multiple colons", 47 | input: "account:kind:ident:ifier:extra", 48 | want: []string{"account", "kind", "ident:ifier:extra"}, 49 | }, 50 | { 51 | name: "ambiguous full or partial id", 52 | // This is ambiguous, but we should treat it as a full id 53 | input: "some:variable:name", 54 | want: []string{"some", "variable", "name"}, 55 | }, 56 | } 57 | 58 | for _, tc := range testCases { 59 | t.Run(tc.name, func(t *testing.T) { 60 | account, kind, id := unopinionatedParseID(tc.input) 61 | got := []string{account, kind, id} 62 | assert.Equal(t, tc.want, got) 63 | }) 64 | } 65 | } 66 | 67 | func TestMakeFullID(t *testing.T) { 68 | testCases := []struct { 69 | name string 70 | input []string 71 | want string 72 | }{ 73 | { 74 | name: "simple full id", 75 | input: []string{"account", "kind", "identifier"}, 76 | want: "account:kind:identifier", 77 | }, 78 | { 79 | name: "simple kind and identifier", 80 | input: []string{"", "kind", "identifier"}, 81 | want: ":kind:identifier", 82 | }, 83 | { 84 | name: "simple identifier", 85 | input: []string{"", "", "identifier"}, 86 | want: "::identifier", 87 | }, 88 | { 89 | name: "empty string", 90 | input: []string{"", "", ""}, 91 | want: "::", 92 | }, 93 | { 94 | name: "full id with colon", 95 | input: []string{"account", "kind", "ident:ifier"}, 96 | want: "account:kind:ident:ifier", 97 | }, 98 | { 99 | name: "full id with multiple colons", 100 | input: []string{"account", "kind", "ident:ifier:extra"}, 101 | want: "account:kind:ident:ifier:extra", 102 | }, 103 | { 104 | name: "full id in last param", 105 | input: []string{"", "", "account:kind:identifier"}, 106 | want: "account:kind:identifier", 107 | }, 108 | { 109 | name: "full id with colon in last param", 110 | input: []string{"", "", "account:kind:ident:ifier"}, 111 | want: "account:kind:ident:ifier", 112 | }, 113 | { 114 | name: "full id with multiple colons in last param", 115 | input: []string{"", "", "account:kind:ident:ifier:extra"}, 116 | want: "account:kind:ident:ifier:extra", 117 | }, 118 | { 119 | name: "ambiguous full or partial id", 120 | // This is ambiguous, but we should treat it as a full id 121 | input: []string{"", "", "some:variable:name"}, 122 | want: "some:variable:name", 123 | }, 124 | { 125 | name: "ambiguous full or partial id with matching account", 126 | // This is ambiguous, but we should treat it as a full id 127 | input: []string{"account", "variable", "account:variable:name"}, 128 | want: "account:variable:name", 129 | }, 130 | { 131 | name: "ambiguous full or partial id with non-matching account", 132 | // This is ambiguous, but we should treat it as a partial ID since the account doesn't match 133 | input: []string{"account", "variable", "some:variable:name"}, 134 | want: "account:variable:some:variable:name", 135 | }, 136 | { 137 | name: "ambiguous full or partial id with non-matching kind", 138 | // This is ambiguous, but we should treat it as a partial ID since the kind doesn't match 139 | input: []string{"account", "variable", "account:kind:name"}, 140 | want: "account:variable:account:kind:name", 141 | }, 142 | } 143 | 144 | for _, tc := range testCases { 145 | t.Run(tc.name, func(t *testing.T) { 146 | got := makeFullID(tc.input[0], tc.input[1], tc.input[2]) 147 | assert.Equal(t, tc.want, got) 148 | }) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /conjurapi/environment_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestEnvironmentType_Set(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | e EnvironmentType 14 | value string 15 | wantErr assert.ErrorAssertionFunc 16 | }{{ 17 | name: "Empty", 18 | e: EnvironmentType(""), 19 | value: "", 20 | wantErr: assert.Error, 21 | }, { 22 | name: "Invalid", 23 | e: EnvironmentType(""), 24 | value: "invalid", 25 | wantErr: assert.Error, 26 | }, { 27 | name: "Set to cloud", 28 | e: EnvironmentSaaS, 29 | value: "cloud", 30 | wantErr: assert.NoError, 31 | }, { 32 | name: "Set to cloud short", 33 | e: EnvironmentSaaS, 34 | value: "CC", 35 | wantErr: assert.NoError, 36 | }, { 37 | name: "Set to enterprise", 38 | e: EnvironmentSH, 39 | value: "enterprise", 40 | wantErr: assert.NoError, 41 | }, { 42 | name: "Set to enterprise short", 43 | e: EnvironmentSH, 44 | value: "CE", 45 | wantErr: assert.NoError, 46 | }, { 47 | name: "Set to oss", 48 | e: EnvironmentOSS, 49 | value: "oss", 50 | wantErr: assert.NoError, 51 | }, { 52 | name: "Set to oss short", 53 | e: EnvironmentOSS, 54 | value: "OSS", 55 | wantErr: assert.NoError, 56 | }, { 57 | name: "Set to open-source", 58 | e: EnvironmentOSS, 59 | value: "open-source", 60 | wantErr: assert.NoError, 61 | }} 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | tt.wantErr(t, tt.e.Set(tt.value), fmt.Sprintf("Set(%v)", tt.value)) 65 | }) 66 | } 67 | } 68 | 69 | func TestEnvironmentType_String(t *testing.T) { 70 | tests := []struct { 71 | name string 72 | e EnvironmentType 73 | want string 74 | }{{ 75 | name: "Empty", 76 | e: EnvironmentType(""), 77 | want: "", 78 | }, { 79 | name: "SaaS", 80 | e: EnvironmentSaaS, 81 | want: "saas", 82 | }, { 83 | name: "Self-Hosted", 84 | e: EnvironmentSH, 85 | want: "self-hosted", 86 | }, { 87 | name: "OSS", 88 | e: EnvironmentOSS, 89 | want: "oss", 90 | }} 91 | for _, tt := range tests { 92 | t.Run(tt.name, func(t *testing.T) { 93 | assert.Equalf(t, tt.want, tt.e.String(), "String()") 94 | }) 95 | } 96 | } 97 | 98 | func Test_defaultEnvironment(t *testing.T) { 99 | tests := []struct { 100 | name string 101 | url string 102 | want EnvironmentType 103 | }{{ 104 | name: "Empty", 105 | url: "", 106 | want: EnvironmentSH, 107 | }, { 108 | name: "Secrets Manager SaaS", 109 | url: "https://tenant.secretsmgr.cyberark.cloud", 110 | want: EnvironmentSaaS, 111 | }} 112 | for _, tt := range tests { 113 | t.Run(tt.name, func(t *testing.T) { 114 | assert.Equalf(t, tt.want, defaultEnvironment(tt.url, false), "defaultEnvironment(%v)", tt.url) 115 | }) 116 | } 117 | } 118 | 119 | func Test_environmentIsSupported(t *testing.T) { 120 | tests := []struct { 121 | name string 122 | want bool 123 | }{ 124 | {"", false}, 125 | {"cloud", false}, 126 | {"saas", true}, 127 | {"CC", false}, 128 | {"enterprise", false}, 129 | {"self-hosted", true}, 130 | {"CE", false}, 131 | {"oss", true}, 132 | {"OSS", true}, 133 | {"open-source", false}, 134 | {"invalid", false}, 135 | } 136 | for _, tt := range tests { 137 | t.Run(tt.name, func(t *testing.T) { 138 | assert.Equalf(t, tt.want, environmentIsSupported(tt.name), "environmentIsSupported(%v)", tt.name) 139 | }) 140 | } 141 | } 142 | 143 | func TestEnvironmentType_FullName(t *testing.T) { 144 | tests := []struct { 145 | name string 146 | e EnvironmentType 147 | want string 148 | }{{ 149 | "Empty", 150 | EnvironmentType(""), 151 | "Unknown Environment", 152 | }, { 153 | name: "Cloud", 154 | e: EnvironmentSaaS, 155 | want: "Secrets Manager SaaS", 156 | }, { 157 | name: "Enterprise", 158 | e: EnvironmentSH, 159 | want: "Secrets Manager Self-Hosted", 160 | }, { 161 | name: "OSS", 162 | e: EnvironmentOSS, 163 | want: "Conjur Open Source", 164 | }, { 165 | name: "SaaS", 166 | e: EnvironmentSaaS, 167 | want: "Secrets Manager SaaS", 168 | }, { 169 | name: "Self-Hosted", 170 | e: EnvironmentSH, 171 | want: "Secrets Manager Self-Hosted", 172 | }, { 173 | name: "Unknown", 174 | e: EnvironmentType("unknown"), 175 | want: "Unknown Environment", 176 | }} 177 | for _, tt := range tests { 178 | t.Run(tt.name, func(t *testing.T) { 179 | assert.Equalf(t, tt.want, tt.e.FullName(), "FullName()") 180 | }) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /conjurapi/policy.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/cyberark/conjur-api-go/conjurapi/response" 9 | ) 10 | 11 | // PolicyMode defines the server-sized behavior when loading a policy. 12 | type PolicyMode uint 13 | 14 | const ( 15 | // PolicyModePost appends new data to the policy. 16 | PolicyModePost PolicyMode = 1 17 | // PolicyModePut completely replaces the policy, implicitly deleting data which is not present in the new policy. 18 | PolicyModePut PolicyMode = 2 19 | // PolicyModePatch adds policy data and explicitly deletes policy data. 20 | PolicyModePatch PolicyMode = 3 21 | ) 22 | 23 | // CreatedRole contains the full role ID and API key of a role which was created 24 | // by the server when loading a policy. 25 | type CreatedRole struct { 26 | ID string `json:"id"` 27 | APIKey string `json:"api_key,omitempty"` 28 | } 29 | 30 | // PolicyResponse contains information about the policy update. 31 | type PolicyResponse struct { 32 | // Newly created roles. 33 | CreatedRoles map[string]CreatedRole `json:"created_roles"` 34 | // The version number of the policy. 35 | Version uint32 `json:"version"` 36 | } 37 | 38 | // DryRunPolicyResponseItems contains Conjur Resources. 39 | type DryRunPolicyResponseItems struct { 40 | Items []Resource `json:"items"` 41 | } 42 | 43 | // DryRunError contains information about any errors that occurred during 44 | // policy validation. 45 | type DryRunError struct { 46 | Line int `json:"line"` 47 | Column int `json:"column"` 48 | Message string `json:"message"` 49 | } 50 | 51 | // DryRunPolicyUpdates defines the specific policy dry run response details on 52 | // which policy updates are modified by a policy load. 53 | type DryRunPolicyUpdates struct { 54 | Before DryRunPolicyResponseItems `json:"before"` 55 | After DryRunPolicyResponseItems `json:"after"` 56 | } 57 | 58 | // DryRunPolicyResponse contains information about the policy validation and 59 | // whether it was successful. 60 | type DryRunPolicyResponse struct { 61 | // Status of the policy validation. 62 | Status string `json:"status"` 63 | Created DryRunPolicyResponseItems `json:"created"` 64 | Updated DryRunPolicyUpdates `json:"updated"` 65 | Deleted DryRunPolicyResponseItems `json:"deleted"` 66 | Errors []DryRunError `json:"errors"` 67 | } 68 | 69 | // LoadPolicy submits new policy data or policy changes to the server. 70 | // 71 | // The required permission depends on the mode. 72 | func (c *Client) LoadPolicy(mode PolicyMode, policyID string, policy io.Reader) (*PolicyResponse, error) { 73 | req, err := c.LoadPolicyRequest(mode, policyID, policy, false) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | resp, err := c.SubmitRequest(req) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | policyResponse := PolicyResponse{} 84 | return &policyResponse, response.JSONResponse(resp, &policyResponse) 85 | } 86 | 87 | func (c *Client) DryRunPolicy(mode PolicyMode, policyID string, policy io.Reader) (*DryRunPolicyResponse, error) { 88 | if isConjurCloudURL(c.config.ApplianceURL) { 89 | return nil, errors.New("Policy Dry Run is not supported in Secrets Manager SaaS") 90 | } 91 | err := c.VerifyMinServerVersion("1.21.1") 92 | if err != nil { 93 | return nil, fmt.Errorf("Policy Dry Run is not supported in Secrets Manager versions older than 1.21.1") 94 | } 95 | 96 | req, err := c.LoadPolicyRequest(mode, policyID, policy, true) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | resp, err := c.SubmitRequest(req) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | policyResponse := DryRunPolicyResponse{} 107 | return &policyResponse, response.DryRunPolicyJSONResponse(resp, &policyResponse) 108 | } 109 | 110 | // FetchPolicy creates a request to fetch policy from the system 111 | func (c *Client) FetchPolicy(policyID string, returnJSON bool, policyTreeDepth uint, sizeLimit uint) ([]byte, error) { 112 | if isConjurCloudURL(c.config.ApplianceURL) { 113 | return nil, errors.New("Policy Fetch is not supported in Secrets Manager SaaS") 114 | } 115 | err := c.VerifyMinServerVersion("1.21.1") 116 | if err != nil { 117 | return nil, fmt.Errorf("Policy Fetch is not supported in Secrets Manager versions older than 1.21.1") 118 | } 119 | 120 | req, err := c.fetchPolicyRequest(policyID, returnJSON, policyTreeDepth, sizeLimit) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | resp, err := c.SubmitRequest(req) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | return response.DataResponse(resp) 131 | } 132 | -------------------------------------------------------------------------------- /conjurapi/authn/token_file_authenticator_test.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func ensureWriteFile(filepath, filecontents string) { 14 | var prevModTime time.Time 15 | 16 | info, err := os.Stat(filepath) 17 | // Panic for any error that is not! NotExist 18 | if err != nil && !os.IsNotExist(err) { 19 | panic(err) 20 | } 21 | 22 | // Register the previous ModTime, otherwise there is no previous file so fall back to a second before this is 23 | if err != nil { 24 | prevModTime = info.ModTime() 25 | } else { 26 | prevModTime = time.Now().Add(-time.Second) 27 | } 28 | 29 | err = os.WriteFile(filepath, []byte(filecontents), 0600) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | timeout := time.After(10 * time.Second) 35 | ticker := time.NewTicker(10 * time.Millisecond) 36 | for { 37 | select { 38 | // Timeout after 10 seconds. Clearly there's something wrong with i/o 39 | case <-timeout: 40 | err := fmt.Errorf("ensureWriteFile timed out.") 41 | 42 | panic(err) 43 | // Return only if the current ModTime is greater than the previous ModTime 44 | case <-ticker.C: 45 | info, err := os.Stat(filepath) 46 | if err != nil || !info.ModTime().After(prevModTime) { 47 | continue 48 | } 49 | 50 | return 51 | } 52 | } 53 | } 54 | 55 | func TestTokenFileAuthenticator_RefreshToken(t *testing.T) { 56 | t.Run("Retrieve existent token file", func(t *testing.T) { 57 | token_file, _ := os.CreateTemp("", "existent-token-file") 58 | token_file_name := token_file.Name() 59 | defer os.Remove(token_file_name) 60 | 61 | token_file_contents := "token-from-file-contents" 62 | ensureWriteFile(token_file_name, token_file_contents) 63 | 64 | authenticator := TokenFileAuthenticator{ 65 | MaxWaitTime: 1 * time.Second, 66 | TokenFile: token_file_name, 67 | } 68 | 69 | token, err := authenticator.RefreshToken() 70 | 71 | assert.NoError(t, err) 72 | assert.Equal(t, "token-from-file-contents", string(token)) 73 | }) 74 | 75 | t.Run("Retrieve eventually existent token file", func(t *testing.T) { 76 | token_dir := t.TempDir() 77 | token_file_name := path.Join(token_dir, "token") 78 | 79 | token_file_contents := "token-from-file-contents" 80 | go func() { 81 | os.WriteFile(token_file_name, []byte(token_file_contents), 0600) 82 | }() 83 | 84 | authenticator := TokenFileAuthenticator{ 85 | TokenFile: token_file_name, 86 | MaxWaitTime: 10 * time.Second, // The write takes place in a go routine so we need to account for slow i/o 87 | } 88 | 89 | token, err := authenticator.RefreshToken() 90 | 91 | assert.NoError(t, err) 92 | assert.Equal(t, "token-from-file-contents", string(token)) 93 | }) 94 | 95 | t.Run("Times out on never-existent token file", func(t *testing.T) { 96 | token_file := "/path/to/non-existent-token-file" 97 | 98 | authenticator := TokenFileAuthenticator{ 99 | TokenFile: token_file, 100 | MaxWaitTime: 10 * time.Millisecond, // Something non-zero, since zero means immediate failure 101 | } 102 | 103 | token, err := authenticator.RefreshToken() 104 | 105 | assert.Nil(t, token) 106 | assert.Error(t, err) 107 | assert.Equal(t, "Operation waitForTextFile timed out.", err.Error()) 108 | }) 109 | 110 | t.Run("Doesn't time out if MaxWaitTime is -1", func(t *testing.T) { 111 | tempDir := t.TempDir() 112 | token_file_name := path.Join(tempDir, "token") 113 | 114 | go func() { 115 | // Wait some time before writing the file 116 | time.Sleep(500 * time.Millisecond) 117 | token_file_contents := "token-from-file-contents" 118 | os.WriteFile(token_file_name, []byte(token_file_contents), 0600) 119 | }() 120 | 121 | authenticator := TokenFileAuthenticator{ 122 | TokenFile: token_file_name, 123 | MaxWaitTime: -1, // Disable timeout 124 | } 125 | 126 | token, err := authenticator.RefreshToken() 127 | 128 | assert.NoError(t, err) 129 | assert.Equal(t, "token-from-file-contents", string(token)) 130 | }) 131 | } 132 | 133 | func TestTokenFileAuthenticator_NeedsTokenRefresh(t *testing.T) { 134 | t.Run("Token refresh needed after updates", func(t *testing.T) { 135 | token_file, _ := os.CreateTemp("", "existent-token-file") 136 | token_file_name := token_file.Name() 137 | defer os.Remove(token_file_name) 138 | 139 | ensureWriteFile(token_file_name, "token-from-file-contents") 140 | 141 | authenticator := TokenFileAuthenticator{ 142 | TokenFile: token_file_name, 143 | MaxWaitTime: 1 * time.Second, 144 | } 145 | 146 | // Read 147 | _, err := authenticator.RefreshToken() 148 | assert.NoError(t, err) 149 | 150 | // Return false for unmodified file 151 | assert.False(t, authenticator.NeedsTokenRefresh()) 152 | 153 | ensureWriteFile(token_file_name, "recent modification") 154 | 155 | // Return true for modified file 156 | assert.True(t, authenticator.NeedsTokenRefresh()) 157 | }) 158 | } 159 | -------------------------------------------------------------------------------- /conjurapi/issuer_v2.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/cyberark/conjur-api-go/conjurapi/response" 9 | "net/http" 10 | ) 11 | 12 | type IssuerSubject struct { 13 | CommonName string `json:"common_name"` 14 | Organization string `json:"organization,omitempty"` 15 | OrgUnits []string `json:"org_units,omitempty"` 16 | Locality string `json:"locality,omitempty"` 17 | State string `json:"state,omitempty"` 18 | Country string `json:"country,omitempty"` 19 | } 20 | 21 | func (s IssuerSubject) Validate() error { 22 | var errs []error 23 | if s.CommonName == "" { 24 | errs = append(errs, fmt.Errorf("Missing required Subject attribute CommonName")) 25 | } 26 | if len(errs) > 0 { 27 | return errors.Join(errs...) 28 | } 29 | return nil 30 | } 31 | 32 | type AltNames struct { 33 | DNSNames []string `json:"dns_names,omitempty"` 34 | IPAddresses []string `json:"ip_addresses,omitempty"` 35 | EMailAddresses []string `json:"email_addresses,omitempty"` 36 | Uris []string `json:"uris,omitempty"` 37 | } 38 | 39 | type Issue struct { 40 | Subject IssuerSubject `json:"subject"` 41 | KeyType string `json:"key_type,omitempty"` 42 | AltNames AltNames `json:"alt_names,omitempty"` 43 | TTL string `json:"ttl,omitempty"` 44 | Zone string `json:"zone,omitempty"` 45 | IgnoreStorage bool `json:"ignore_storage,omitempty"` 46 | } 47 | 48 | func (i Issue) Validate() error { 49 | return i.Subject.Validate() 50 | } 51 | 52 | type CertificateResponse struct { 53 | Certificate string `json:"certificate,omitempty"` 54 | Chain []string `json:"chain,omitempty"` 55 | PrivateKey string `json:"private_key,omitempty"` 56 | } 57 | 58 | type Sign struct { 59 | Csr string `json:"csr"` 60 | Zone string `json:"zone,omitempty"` 61 | TTL string `json:"ttl,omitempty"` 62 | } 63 | 64 | func (s Sign) Validate() error { 65 | var errs []error 66 | if s.Csr == "" { 67 | errs = append(errs, fmt.Errorf("Missing required Sign attribute csr")) 68 | } 69 | if len(errs) > 0 { 70 | return errors.Join(errs...) 71 | } 72 | return nil 73 | } 74 | 75 | func (c *ClientV2) CertificateIssueRequest(issuerName string, issue Issue) (*http.Request, error) { 76 | err := issue.Validate() 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | err = issue.Subject.Validate() 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | issueJSON, err := json.Marshal(issue) 87 | 88 | path := fmt.Sprintf("issuers/%s/issue", issuerName) 89 | //path := "issue" 90 | 91 | c.issuersURL(c.config.Account) 92 | branchURL := makeRouterURL(c.config.ApplianceURL, path).String() 93 | 94 | request, err := http.NewRequest( 95 | http.MethodPost, 96 | branchURL, 97 | bytes.NewBuffer(issueJSON), 98 | ) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | request.Header.Add(v2APIOutgoingHeaderID, v2APIHeaderBeta) 104 | request.Header.Add("Content-Type", "application/json") 105 | 106 | return request, nil 107 | } 108 | 109 | func (c *ClientV2) CertificateIssue(issuerName string, issue Issue) (*CertificateResponse, error) { 110 | if !isConjurCloudURL(c.config.ApplianceURL) { 111 | return nil, fmt.Errorf("Issue API %s", NotSupportedInConjurEnterprise) 112 | } 113 | 114 | req, err := c.CertificateIssueRequest(issuerName, issue) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | resp, err := c.SubmitRequest(req) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | bodyData, err := response.DataResponse(resp) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | issueResp := CertificateResponse{} 130 | err = json.Unmarshal(bodyData, &issueResp) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | return &issueResp, nil 136 | } 137 | 138 | func (c *ClientV2) CertificateSignRequest(issuerName string, sign Sign) (*http.Request, error) { 139 | err := sign.Validate() 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | signJSON, err := json.Marshal(sign) 145 | 146 | path := fmt.Sprintf("issuers/%s/sign", issuerName) 147 | 148 | branchURL := makeRouterURL(c.config.ApplianceURL, path).String() 149 | 150 | request, err := http.NewRequest( 151 | http.MethodPost, 152 | branchURL, 153 | bytes.NewBuffer(signJSON), 154 | ) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | request.Header.Add(v2APIOutgoingHeaderID, v2APIHeaderBeta) 160 | request.Header.Add("Content-Type", "application/json") 161 | 162 | return request, nil 163 | } 164 | 165 | func (c *ClientV2) CertificateSign(issuerName string, sign Sign) (*CertificateResponse, error) { 166 | if !isConjurCloudURL(c.config.ApplianceURL) { 167 | return nil, fmt.Errorf("Issue API %s", NotSupportedInConjurEnterprise) 168 | } 169 | 170 | req, err := c.CertificateSignRequest(issuerName, sign) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | resp, err := c.SubmitRequest(req) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | bodyData, err := response.DataResponse(resp) 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | issueResp := CertificateResponse{} 186 | err = json.Unmarshal(bodyData, &issueResp) 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | return &issueResp, nil 192 | } 193 | -------------------------------------------------------------------------------- /conjurapi/authn_gcp_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/cyberark/conjur-api-go/conjurapi/authn" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | var authnGcpPolicy = ` 16 | - !webservice 17 | 18 | - !group apps 19 | 20 | - !permit 21 | role: !group apps 22 | privilege: [ read, authenticate ] 23 | resource: !webservice 24 | 25 | # Give the host permission to authenticate using the GCP Authenticator 26 | - !grant 27 | role: !group apps 28 | member: !host /data/test/gcp-apps/test-app 29 | ` 30 | var authGcpRolesPolicy = ` 31 | - !policy 32 | id: gcp-apps 33 | body: 34 | - &variables 35 | - !variable database/username 36 | - !variable database/password 37 | # Create a group that will have permission to retrieve variables 38 | - !group secrets-users 39 | # Give the secrets-users group permission to retrieve variables 40 | - !permit 41 | role: !group secrets-users 42 | privilege: [ read, execute ] 43 | resource: *variables 44 | 45 | # Create a group to hold this application's hosts 46 | - !group 47 | - !host 48 | id: test-app 49 | annotations: 50 | authn-gcp/project-id: {{ PROJECT_ID }} 51 | # Add our host into our group 52 | - !grant 53 | role: !group 54 | member: !host test-app 55 | # Give the host in our group permission to retrieve variables 56 | - !grant 57 | member: !group 58 | role: !group secrets-users 59 | ` 60 | 61 | func TestGCPAuthenticatorRefreshJWT(t *testing.T) { 62 | authenticator := &authn.GCPAuthenticator{ 63 | JWT: "explicit-token", 64 | Authenticate: func(jwt string) ([]byte, error) { 65 | assert.Equal(t, "explicit-token", jwt) 66 | return []byte("fake-token"), nil 67 | }, 68 | } 69 | 70 | err := authenticator.RefreshJWT() 71 | require.NoError(t, err) 72 | assert.Equal(t, "explicit-token", authenticator.JWT) 73 | } 74 | 75 | func TestAuthnGCP(t *testing.T) { 76 | // Only run this if explicitly enabled 77 | if strings.ToLower(os.Getenv("TEST_GCP")) != "true" { 78 | t.Skip("Skipping GCP authn test") 79 | } 80 | 81 | // Replace placeholder in policy with actual project ID 82 | projectID := os.Getenv("GCP_PROJECT_ID") 83 | if projectID == "" { 84 | t.Fatal("GCP_PROJECT_ID environment variable is not set") 85 | } 86 | authGcpRolesPolicy = strings.ReplaceAll(authGcpRolesPolicy, "{{ PROJECT_ID }}", projectID) 87 | 88 | testCases := []struct { 89 | name string 90 | useExplicitToken bool 91 | }{ 92 | { 93 | name: "Happy path with stubbed metadata server", 94 | useExplicitToken: false, 95 | }, 96 | { 97 | name: "Happy path with explicit token", 98 | useExplicitToken: true, 99 | }, 100 | } 101 | 102 | // Run a stub HTTP server and set the metadata URL to point to it: 103 | // this is necessary because GCP agents lack Docker runtime, 104 | // so the test must be run on a non GCP agent (e.g. on AWS). 105 | const metadataEndpointUri = "/test-identity" 106 | 107 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 108 | if r.Method == http.MethodGet && r.URL.Path == metadataEndpointUri { 109 | w.Header().Set(authn.GcpMetadataFlavorHeaderName, authn.GcpMetadataFlavorHeaderValue) 110 | w.WriteHeader(http.StatusOK) 111 | gcpToken := os.Getenv("GCP_ID_TOKEN") 112 | if gcpToken == "" { 113 | t.Fatal("GCP_ID_TOKEN environment variable is not set") 114 | } 115 | w.Write([]byte(gcpToken)) 116 | return 117 | } 118 | http.NotFound(w, r) 119 | })) 120 | defer server.Close() 121 | 122 | for _, tc := range testCases { 123 | t.Run(tc.name, func(t *testing.T) { 124 | 125 | utils, err := NewTestUtils(&Config{}) 126 | require.NoError(t, err) 127 | 128 | err = utils.SetupWithAuthenticator("gcp", authnGcpPolicy, authGcpRolesPolicy) 129 | require.NoError(t, err) 130 | conjur := utils.Client() 131 | conjur.EnableAuthenticator("gcp", "", true) 132 | 133 | err = conjur.AddSecret("data/test/gcp-apps/database/username", "secret") 134 | require.NoError(t, err) 135 | err = conjur.AddSecret("data/test/gcp-apps/database/password", "P@ssw0rd!") 136 | require.NoError(t, err) 137 | 138 | // EXERCISE 139 | jwtContent := "" 140 | gcpURL := "" 141 | if tc.useExplicitToken { 142 | jwtContent = os.Getenv("GCP_ID_TOKEN") 143 | gcpURL = authn.GcpIdentityURL 144 | } else { 145 | gcpURL = server.URL + metadataEndpointUri 146 | } 147 | config := Config{ 148 | ApplianceURL: conjur.config.ApplianceURL, 149 | Account: conjur.config.Account, 150 | AuthnType: "gcp", 151 | JWTHostID: "data/test/gcp-apps/test-app", 152 | JWTContent: jwtContent, 153 | } 154 | gcpConjur, err := NewClientFromGCPCredentials(config, gcpURL) 155 | require.NoError(t, err) 156 | 157 | _, err = gcpConjur.GetAuthenticator().RefreshToken() 158 | require.NoError(t, err) 159 | 160 | whoami, err := gcpConjur.WhoAmI() 161 | assert.NoError(t, err) 162 | assert.Contains(t, string(whoami), config.JWTHostID) 163 | 164 | secret, err := gcpConjur.RetrieveSecret("data/test/gcp-apps/database/username") 165 | assert.NoError(t, err) 166 | assert.Equal(t, "secret", string(secret)) 167 | 168 | secret, err = gcpConjur.RetrieveSecret("data/test/gcp-apps/database/password") 169 | assert.NoError(t, err) 170 | assert.Equal(t, "P@ssw0rd!", string(secret)) 171 | }) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /conjurapi/variable.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/cyberark/conjur-api-go/conjurapi/response" 11 | ) 12 | 13 | // RetrieveBatchSecrets fetches values for all variables in a slice using a 14 | // single API call 15 | // 16 | // The authenticated user must have execute privilege on all variables. 17 | func (c *Client) RetrieveBatchSecrets(variableIDs []string) (map[string][]byte, error) { 18 | jsonResponse, err := c.retrieveBatchSecrets(variableIDs, false) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | resolvedVariables := map[string][]byte{} 24 | for id, value := range jsonResponse { 25 | resolvedVariables[id] = []byte(value) 26 | } 27 | 28 | return resolvedVariables, nil 29 | } 30 | 31 | // RetrieveBatchSecretsSafe fetches values for all variables in a slice using a 32 | // single API call. This version of the method will automatically base64-encode 33 | // the secrets on the server side allowing the retrieval of binary values in 34 | // batch requests. Secrets are NOT base64 encoded in the returned map. 35 | // 36 | // The authenticated user must have execute privilege on all variables. 37 | func (c *Client) RetrieveBatchSecretsSafe(variableIDs []string) (map[string][]byte, error) { 38 | jsonResponse, err := c.retrieveBatchSecrets(variableIDs, true) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return decodeBase64Values(jsonResponse) 44 | } 45 | 46 | // RetrieveSecret fetches a secret from a variable. 47 | // 48 | // The authenticated user must have execute privilege on the variable. 49 | func (c *Client) RetrieveSecret(variableID string) ([]byte, error) { 50 | resp, err := c.retrieveSecret(variableID) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return response.DataResponse(resp) 56 | } 57 | 58 | // RetrieveSecretReader fetches a secret from a variable and returns it as a 59 | // data stream. 60 | // 61 | // The authenticated user must have execute privilege on the variable. 62 | func (c *Client) RetrieveSecretReader(variableID string) (io.ReadCloser, error) { 63 | resp, err := c.retrieveSecret(variableID) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return response.SecretDataResponse(resp) 69 | } 70 | 71 | // RetrieveSecretWithVersion fetches a specific version of a secret from a 72 | // variable. 73 | // 74 | // The authenticated user must have execute privilege on the variable. 75 | func (c *Client) RetrieveSecretWithVersion(variableID string, version int) ([]byte, error) { 76 | resp, err := c.retrieveSecretWithVersion(variableID, version) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return response.DataResponse(resp) 82 | } 83 | 84 | // RetrieveSecretWithVersionReader fetches a specific version of a secret from a 85 | // variable and returns it as a data stream. 86 | // 87 | // The authenticated user must have execute privilege on the variable. 88 | func (c *Client) RetrieveSecretWithVersionReader(variableID string, version int) (io.ReadCloser, error) { 89 | resp, err := c.retrieveSecretWithVersion(variableID, version) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | return response.SecretDataResponse(resp) 95 | } 96 | 97 | func (c *Client) retrieveBatchSecrets(variableIDs []string, base64Flag bool) (map[string]string, error) { 98 | req, err := c.RetrieveBatchSecretsRequest(variableIDs, base64Flag) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | resp, err := c.SubmitRequest(req) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | data, err := response.DataResponse(resp) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | if base64Flag && resp.Header.Get("Content-Encoding") != "base64" { 114 | return nil, errors.New( 115 | "Conjur response is not Base64-encoded. " + 116 | "The Conjur version may not be compatible with this function - " + 117 | "try using RetrieveBatchSecrets instead.") 118 | } 119 | 120 | jsonResponse := map[string]string{} 121 | err = json.Unmarshal(data, &jsonResponse) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | return jsonResponse, nil 127 | } 128 | 129 | func (c *Client) retrieveSecret(variableID string) (*http.Response, error) { 130 | req, err := c.RetrieveSecretRequest(variableID) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | return c.SubmitRequest(req) 136 | } 137 | 138 | func (c *Client) retrieveSecretWithVersion(variableID string, version int) (*http.Response, error) { 139 | req, err := c.RetrieveSecretWithVersionRequest(variableID, version) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | return c.SubmitRequest(req) 145 | } 146 | 147 | // AddSecret adds a secret value to a variable. 148 | // 149 | // The authenticated user must have update privilege on the variable. 150 | func (c *Client) AddSecret(variableID string, secretValue string) error { 151 | req, err := c.AddSecretRequest(variableID, secretValue) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | resp, err := c.SubmitRequest(req) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | return response.EmptyResponse(resp) 162 | } 163 | 164 | func decodeBase64Values(jsonResponse map[string]string) (map[string][]byte, error) { 165 | resolvedVariables := map[string][]byte{} 166 | for id, value := range jsonResponse { 167 | decodedValue, err := base64.StdEncoding.DecodeString(value) 168 | if err != nil { 169 | return nil, err 170 | } 171 | resolvedVariables[id] = decodedValue 172 | } 173 | return resolvedVariables, nil 174 | } 175 | -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | top_level_path="$(pwd)" 4 | cd "$(dirname "$0")" 5 | source ./utils.sh 6 | 7 | trap teardown EXIT 8 | 9 | # unique for pipeline; repeatable & meaningful for dev 10 | PROJECT_SUFFIX="$(openssl rand -hex 3)" 11 | 12 | # To selectively choose packages and tests, export these envs before call: 13 | # API_PKGS such as "./..." or "./conjurapi" 14 | # will be passed like 'go test -v ' 15 | # API_TESTS such as "TestClient_LoadPolicy" 16 | # will be used like 'go test -run ' 17 | 18 | # default to no tests specified, all packages 19 | PKGS="-v ./..." 20 | TESTS="" 21 | if [ "$API_PKGS" != "" ]; then 22 | PKGS="-v ${API_PKGS}" 23 | fi 24 | if [ "$API_TESTS" != "" ]; then 25 | TESTS="-run ${API_TESTS}" 26 | PROJECT_SUFFIX=$(project_nameable "$API_TESTS") 27 | fi 28 | 29 | export COMPOSE_PROJECT_NAME="conjurapigo_${PROJECT_SUFFIX}" 30 | export GO_VERSION="${1:-"1.24"}" 31 | export REGISTRY_URL="${2:-docker.io}" 32 | export TEST_AWS="${INFRAPOOL_TEST_AWS:-false}" 33 | export TEST_AZURE="${INFRAPOOL_TEST_AZURE:-false}" 34 | export TEST_GCP="${INFRAPOOL_TEST_GCP:-false}" 35 | 36 | if [[ "$TEST_GCP" == "true" ]]; then 37 | export GCP_CTX_DIR="${3:-gcp}" 38 | GCP_PROJECT_ID="" 39 | GCP_ID_TOKEN="" 40 | if [[ -f "$top_level_path/$GCP_CTX_DIR/project-id" ]]; then 41 | read -r GCP_PROJECT_ID < "$top_level_path/$GCP_CTX_DIR/project-id" 42 | fi 43 | if [[ -f "$top_level_path/$GCP_CTX_DIR/token" ]]; then 44 | read -r GCP_ID_TOKEN < "$top_level_path/$GCP_CTX_DIR/token" 45 | fi 46 | if [[ -z "$GCP_PROJECT_ID" || -z "$GCP_ID_TOKEN" ]]; then 47 | echo "GCP_PROJECT_ID and GCP_ID_TOKEN must be set to run GCP tests" 48 | failed 49 | fi 50 | export GCP_PROJECT_ID 51 | export GCP_ID_TOKEN 52 | fi 53 | 54 | echo "REGISTRY_URL is set to: $REGISTRY_URL" 55 | 56 | init_jwt_server 57 | 58 | if [ -z "$INFRAPOOL_TEST_CLOUD" ]; then 59 | # Spin up Conjur environment 60 | source ./start-conjur.sh 61 | 62 | announce "Building test containers..." 63 | docker compose build "test-$GO_VERSION" 64 | echo "Done!" 65 | 66 | # generate output folder locally, if needed 67 | output_dir="../output/$GO_VERSION" 68 | mkdir -p $output_dir 69 | 70 | # We are using package list mode of 'go test' 71 | # note: the expression must be eval'ed before passing to docker 72 | export TEST_PKGS="$PKGS $TESTS" 73 | 74 | announce "Running tests for Go version: $GO_VERSION..."; 75 | echo "Package and test selection: $TEST_PKGS" 76 | 77 | docker compose run \ 78 | --rm \ 79 | --no-deps \ 80 | -e CONJUR_AUTHN_API_KEY \ 81 | -e TEST_AWS \ 82 | -e TEST_AZURE \ 83 | -e AZURE_SUBSCRIPTION_ID \ 84 | -e AZURE_RESOURCE_GROUP \ 85 | -e USER_ASSIGNED_IDENTITY \ 86 | -e USER_ASSIGNED_IDENTITY_CLIENT_ID \ 87 | -e TEST_GCP \ 88 | -e GCP_PROJECT_ID \ 89 | -e GCP_ID_TOKEN \ 90 | -e GO_VERSION \ 91 | -e PUBLIC_KEYS \ 92 | -e JWT \ 93 | -e TEST_PKGS \ 94 | "test-$GO_VERSION" bash -c 'set -o pipefail; 95 | echo "Go version: $(go version)" 96 | output_dir="./output/$GO_VERSION" 97 | go test -coverprofile="$output_dir/c.out" $TEST_PKGS | tee "$output_dir/junit.output"; 98 | exit_code=$?; 99 | echo "Tests finished - aggregating results..."; 100 | cat "$output_dir/junit.output" | go-junit-report > "$output_dir/junit.xml"; 101 | gocov convert "$output_dir/c.out" | gocov-xml > "$output_dir/coverage.xml"; 102 | [ "$exit_code" -eq 0 ]' || failed 103 | else 104 | # Export INFRAPOOL env vars for Cloud tests 105 | export CONJUR_APPLIANCE_URL="$INFRAPOOL_CONJUR_APPLIANCE_URL/api" 106 | export CONJUR_ACCOUNT=conjur 107 | export CONJUR_AUTHN_LOGIN=$INFRAPOOL_CONJUR_AUTHN_LOGIN 108 | export CONJUR_AUTHN_TOKEN=$(echo "$INFRAPOOL_CONJUR_AUTHN_TOKEN" | base64 --decode) 109 | export IDENTITY_TOKEN=$INFRAPOOL_IDENTITY_TOKEN 110 | 111 | output_dir="../output/cloud" 112 | mkdir -p $output_dir 113 | 114 | docker build \ 115 | --build-arg FROM_IMAGE="golang:$GO_VERSION" \ 116 | -t "test-$GO_VERSION" .. 117 | 118 | announce "Running Secrets Manager SaaS tests for Go version: $GO_VERSION..."; 119 | # NOTE: Skipping hostfactory token tests as hostfactory endpoints seem to be disabled by default now 120 | docker run \ 121 | -e CONJUR_APPLIANCE_URL \ 122 | -e CONJUR_ACCOUNT \ 123 | -e CONJUR_AUTHN_LOGIN \ 124 | -e CONJUR_AUTHN_TOKEN \ 125 | -e TEST_AWS \ 126 | -e TEST_AZURE \ 127 | -e AZURE_SUBSCRIPTION_ID \ 128 | -e AZURE_RESOURCE_GROUP \ 129 | -e USER_ASSIGNED_IDENTITY \ 130 | -e USER_ASSIGNED_IDENTITY_CLIENT_ID \ 131 | -e TEST_GCP \ 132 | -e GCP_PROJECT_ID \ 133 | -e GCP_ID_TOKEN \ 134 | -e PUBLIC_KEYS \ 135 | -e JWT \ 136 | -e IDENTITY_TOKEN \ 137 | -v "$(pwd)/../output:/conjur-api-go/output" \ 138 | "test-$GO_VERSION" bash -c 'set -xo pipefail; 139 | output_dir="./output/cloud" 140 | go test -coverprofile="$output_dir/c.out" -skip "TestClient_Token" -v ./... | tee "$output_dir/junit.output"; 141 | exit_code=$?; 142 | echo "Tests finished - aggregating results..."; 143 | cat "$output_dir/junit.output" | go-junit-report > "$output_dir/junit.xml"; 144 | gocov convert "$output_dir/c.out" | gocov-xml > "$output_dir/coverage.xml"; 145 | gocovmerge "./output/1.24/c.out" "$output_dir/c.out" > "$output_dir/merged-coverage.out"; 146 | gocov convert "$output_dir/merged-coverage.out" | gocov-xml > "$output_dir/merged-coverage.xml"; 147 | [ "$exit_code" -eq 0 ]' || failed 148 | fi 149 | -------------------------------------------------------------------------------- /conjurapi/info.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/cyberark/conjur-api-go/conjurapi/response" 12 | ) 13 | 14 | type EnterpriseInfoResponse struct { 15 | Release string `json:"release"` 16 | Version string `json:"version"` 17 | Services map[string]EnterpriseInfoService `json:"services"` 18 | Container string `json:"container"` 19 | Role string `json:"role"` 20 | Configuration interface{} `json:"configuration"` 21 | Authenticators interface{} `json:"authenticators"` 22 | FipsMode string `json:"fips_mode"` 23 | FeatureFlags interface{} `json:"feature_flags"` 24 | } 25 | 26 | type EnterpriseInfoService struct { 27 | Desired string `json:"desired"` 28 | Status string `json:"status"` 29 | Err string `json:"err"` 30 | Description string `json:"description"` 31 | Name string `json:"name"` 32 | Version string `json:"version"` 33 | Arch string `json:"arch"` 34 | } 35 | 36 | // ServerVersion retrieves the Conjur server version, either from the '/info' endpoint in Secrets Manager Self-Hosted, 37 | // or from the root endpoint in Conjur OSS. The version returned corresponds to the Conjur OSS version, 38 | // which in Conjur Enterprise is the version of the 'possum' service. 39 | func (c *Client) ServerVersion() (string, error) { 40 | if isConjurCloudURL(c.config.ApplianceURL) { 41 | return "", errors.New("Unable to retrieve server version: not supported in Secrets Manager SaaS") 42 | } 43 | 44 | info, err := c.EnterpriseServerInfo() 45 | if err == nil { 46 | // Return the version of the 'possum' service, which corresponds to the Conjur OSS version 47 | return info.Services["possum"].Version, nil 48 | } 49 | 50 | version, err := c.ServerVersionFromRoot() 51 | if err == nil { 52 | return version, nil 53 | } 54 | 55 | return "", fmt.Errorf("failed to retrieve server version: %s", err) 56 | } 57 | 58 | // EnterpriseServerInfo retrieves the server information from the '/info' endpoint. 59 | // This is only available in Conjur Enterprise and will fail with a 404 error in Conjur OSS. 60 | func (c *Client) EnterpriseServerInfo() (*EnterpriseInfoResponse, error) { 61 | if isConjurCloudURL(c.config.ApplianceURL) { 62 | return nil, errors.New("Unable to retrieve server info: not supported in Secrets Manager SaaS") 63 | } 64 | 65 | req, err := c.ServerInfoRequest() 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | resp, err := c.httpClient.Do(req) 71 | // Handle 404 or 401 response, which indicates that the '/info' endpoint is not available (eg. in Conjur OSS) 72 | if resp != nil && (resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusUnauthorized) { 73 | return nil, fmt.Errorf("404 Not Found: Are you using Conjur Enterprise?") 74 | } 75 | 76 | // Handle any other errors 77 | if err != nil { 78 | return nil, fmt.Errorf("failed to retrieve server info: %s", err) 79 | } 80 | 81 | infoResponse := EnterpriseInfoResponse{} 82 | return &infoResponse, response.JSONResponse(resp, &infoResponse) 83 | } 84 | 85 | // ServerVersionFromRoot retrieves the server version from the root endpoint. 86 | // This is a fallback method in case the '/info' endpoint is not available (such as in Conjur OSS). 87 | // In older versions of Conjur, the version was only available in an HTML response, and 88 | // this method will parse it from there. 89 | // In newer Conjur versions, the version is available in a JSON response. 90 | func (c *Client) ServerVersionFromRoot() (string, error) { 91 | if isConjurCloudURL(c.config.ApplianceURL) { 92 | return "", errors.New("Unable to retrieve server version: not supported in Secrets Manager SaaS") 93 | } 94 | 95 | req, err := c.RootRequest() 96 | if err != nil { 97 | return "", err 98 | } 99 | 100 | resp, err := c.httpClient.Do(req) 101 | if err != nil { 102 | return "", err 103 | } 104 | 105 | body, err := response.DataResponse(resp) 106 | if err != nil { 107 | return "", err 108 | } 109 | 110 | serverVersion, err := parseVersionFromRoot(resp, body) 111 | if err != nil { 112 | return "", err 113 | } 114 | 115 | return serverVersion, nil 116 | } 117 | 118 | func parseVersionFromRoot(rootResponse *http.Response, body []byte) (string, error) { 119 | if strings.Contains(rootResponse.Header.Get("content-type"), "application/json") { 120 | return parseVersionFromJSON(body) 121 | } 122 | 123 | return parseVersionFromHTML(string(body)) 124 | } 125 | 126 | func parseVersionFromJSON(jsonContent []byte) (string, error) { 127 | // Parse the body as JSON and look for the version field 128 | var result map[string]interface{} 129 | if err := json.Unmarshal(jsonContent, &result); err != nil { 130 | return "", fmt.Errorf("failed to parse JSON: %s", err) 131 | } 132 | 133 | if version, ok := result["version"].(string); ok { 134 | return version, nil 135 | } 136 | 137 | return "", fmt.Errorf("version field not found") 138 | } 139 | 140 | func parseVersionFromHTML(htmlContent string) (string, error) { 141 | // Parse the body as HTML and look for the version field 142 | // It should look like this: 143 | //
Version 1.21.0.1-25
144 | re := regexp.MustCompile(`
\s*Version\s*([^\s<]+)\s*<\/dd>`) 145 | matches := re.FindStringSubmatch(htmlContent) 146 | // This will return an slice with two elements: The first is the full HTML tag (e.g. "
Version...") 147 | // and the second is just the version number (the capture group in the regex) 148 | if len(matches) < 2 { 149 | return "", fmt.Errorf("version field not found") 150 | } 151 | // Return just the version number 152 | return matches[1], nil 153 | } 154 | -------------------------------------------------------------------------------- /conjurapi/host_factory_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestClient_Token(t *testing.T) { 11 | config := &Config{} 12 | config.mergeEnv() 13 | 14 | var token string 15 | 16 | testCases := []struct { 17 | name string 18 | duration string 19 | hostFactory string 20 | count int 21 | cidr []string 22 | expectNoToken bool 23 | assert func(*testing.T, error) 24 | assertHost func(*testing.T, int, error) 25 | }{ 26 | { 27 | name: "Create a token", 28 | duration: "10m", 29 | hostFactory: "conjur:host_factory:data/test/factory", 30 | count: 1, 31 | cidr: []string{"0.0.0.0/0"}, 32 | assert: func(t *testing.T, err error) { 33 | assert.NoError(t, err) 34 | }, 35 | assertHost: func(t *testing.T, size int, err error) { 36 | assert.NoError(t, err) 37 | assert.True(t, size > 0) 38 | }, 39 | }, 40 | { 41 | name: "Create a token with a partial hostfactory id", 42 | duration: "10m", 43 | hostFactory: "host_factory:data/test/factory", 44 | count: 1, 45 | cidr: []string{"0.0.0.0/0"}, 46 | assert: func(t *testing.T, err error) { 47 | assert.NoError(t, err) 48 | }, 49 | assertHost: func(t *testing.T, size int, err error) { 50 | assert.NoError(t, err) 51 | assert.True(t, size > 0) 52 | }, 53 | }, 54 | { 55 | name: "Create a token with a partial (singular) hostfactory id", 56 | duration: "10m", 57 | hostFactory: "data/test/factory", 58 | count: 1, 59 | cidr: []string{"0.0.0.0/0"}, 60 | assert: func(t *testing.T, err error) { 61 | assert.NoError(t, err) 62 | }, 63 | assertHost: func(t *testing.T, size int, err error) { 64 | assert.NoError(t, err) 65 | assert.True(t, size > 0) 66 | }, 67 | }, 68 | { 69 | name: "Create a token with two cidrs", 70 | duration: "10m", 71 | hostFactory: "conjur:host_factory:data/test/factory", 72 | count: 1, 73 | cidr: []string{"0.0.0.0/0", "0.0.0.0/32"}, 74 | assert: func(t *testing.T, err error) { 75 | assert.NoError(t, err) 76 | }, 77 | assertHost: func(t *testing.T, size int, err error) { 78 | assert.NoError(t, err) 79 | assert.True(t, size > 0) 80 | }, 81 | }, 82 | { 83 | name: "Create a token with empty cidrs", 84 | duration: "10m", 85 | hostFactory: "conjur:host_factory:data/test/factory", 86 | count: 1, 87 | cidr: []string{}, 88 | assert: func(t *testing.T, err error) { 89 | assert.NoError(t, err) 90 | }, 91 | assertHost: func(t *testing.T, size int, err error) { 92 | assert.NoError(t, err) 93 | assert.True(t, size > 0) 94 | }, 95 | }, 96 | { 97 | name: "Create Two tokens", 98 | duration: "10m", 99 | hostFactory: "conjur:host_factory:data/test/factory", 100 | count: 2, 101 | cidr: []string{"0.0.0.0/0", "0.0.0.0/32"}, 102 | assert: func(t *testing.T, err error) { 103 | assert.NoError(t, err) 104 | }, 105 | assertHost: func(t *testing.T, size int, err error) { 106 | assert.NoError(t, err) 107 | assert.True(t, size > 0) 108 | }, 109 | }, 110 | { 111 | name: "Create a token with invalid cidr", 112 | duration: "10m", 113 | hostFactory: "conjur:host_factory:data/test/factory", 114 | count: 1, 115 | cidr: []string{"127.0.0.1"}, 116 | assert: func(t *testing.T, err error) { 117 | assert.NoError(t, err) 118 | }, 119 | assertHost: func(t *testing.T, size int, err error) { 120 | assert.Error(t, err) 121 | }, 122 | }, 123 | { 124 | name: "Invalid duration", 125 | duration: "10", 126 | hostFactory: "conjur:host_factory:data/test/factory", 127 | count: 1, 128 | cidr: []string{"0.0.0.0/0"}, 129 | expectNoToken: true, 130 | assert: func(t *testing.T, err error) { 131 | assert.Error(t, err) 132 | }, 133 | assertHost: func(t *testing.T, size int, err error) { 134 | return 135 | }, 136 | }, 137 | { 138 | name: "Invalid hostfactory id", 139 | duration: "10m", 140 | hostFactory: "conjur:data/test/factory", 141 | count: 1, 142 | cidr: []string{"0.0.0.0/0"}, 143 | expectNoToken: true, 144 | assert: func(t *testing.T, err error) { 145 | assert.Error(t, err) 146 | }, 147 | assertHost: func(t *testing.T, size int, err error) { 148 | return 149 | }, 150 | }, 151 | } 152 | 153 | t.Run("Host Factory", func(t *testing.T) { 154 | identifier := "factory" 155 | policy := fmt.Sprintf(`- !layer lay 156 | - !host-factory 157 | id: %s 158 | layers: [!layer lay]`, identifier) 159 | 160 | utils, err := NewTestUtils(config) 161 | assert.NoError(t, err) 162 | 163 | utils.Setup(policy) 164 | conjur := utils.Client() 165 | 166 | for _, tc := range testCases { 167 | token = "" 168 | t.Run(tc.name, func(t *testing.T) { 169 | tokens, err := conjur.CreateToken(tc.duration, tc.hostFactory, tc.cidr, tc.count) 170 | tc.assert(t, err) 171 | if err == nil { 172 | assert.Equal(t, len(tokens), tc.count) 173 | for _, tokn := range tokens { 174 | // We just save one token if there are multiple 175 | token = tokn.Token 176 | assert.True(t, len(token) > 0) 177 | } 178 | } 179 | }) 180 | if tc.expectNoToken == true { 181 | continue 182 | } 183 | t.Run("Create Host", func(t *testing.T) { 184 | host, err := conjur.CreateHostWithAnnotations("data/test/new-host", token, map[string]string{"authn/api-key": "true", "creator": "me"}) 185 | tc.assertHost(t, len(host.ApiKey), err) 186 | }) 187 | t.Run("Delete Token", func(t *testing.T) { 188 | err = conjur.DeleteToken(token) 189 | assert.NoError(t, err) 190 | }) 191 | } 192 | }) 193 | } 194 | -------------------------------------------------------------------------------- /conjurapi/secret_static_v2.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/cyberark/conjur-api-go/conjurapi/response" 11 | ) 12 | 13 | type Subject struct { 14 | Id string `json:"id"` 15 | Kind string `json:"kind"` 16 | } 17 | 18 | type Permission struct { 19 | Subject Subject `json:"subject,omitempty"` 20 | Privileges []string `json:"privileges,omitempty"` 21 | Href string `json:"href,omitempty"` 22 | } 23 | 24 | type PermissionResponse struct { 25 | Permission []Permission `json:"permissions,omitempty"` 26 | Count int `json:"count"` 27 | } 28 | 29 | type StaticSecret struct { 30 | Branch string `json:"branch"` 31 | Name string `json:"name"` 32 | MimeType string `json:"mime_type,omitempty"` 33 | Owner *Owner `json:"owner,omitempty"` 34 | Value string `json:"value,omitempty"` 35 | Annotations map[string]string `json:"annotations,omitempty"` 36 | Permissions []Permission `json:"permissions,omitempty"` 37 | } 38 | 39 | type StaticSecretResponse struct { 40 | StaticSecret 41 | Permissions Permission `json:"permissions"` 42 | } 43 | 44 | func (c *ClientV2) CreateStaticSecretRequest(secret StaticSecret) (*http.Request, error) { 45 | err := secret.Validate() 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | branchJson, err := json.Marshal(secret) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | secretURL := makeRouterURL(c.config.ApplianceURL, "secrets/static").String() 56 | 57 | request, err := http.NewRequest( 58 | http.MethodPost, 59 | secretURL, 60 | bytes.NewBuffer(branchJson), 61 | ) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | request.Header.Add("Content-Type", "application/json") 67 | request.Header.Add(v2APIOutgoingHeaderID, v2APIHeader) 68 | 69 | return request, nil 70 | } 71 | 72 | func (c *ClientV2) CreateStaticSecret(secret StaticSecret) (*StaticSecretResponse, error) { 73 | if !isConjurCloudURL(c.config.ApplianceURL) { 74 | return nil, fmt.Errorf("StaticSecret API %s", NotSupportedInConjurEnterprise) 75 | } 76 | 77 | req, err := c.CreateStaticSecretRequest(secret) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | resp, err := c.SubmitRequest(req) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | bodyData, err := response.DataResponse(resp) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | secretResp := StaticSecretResponse{} 93 | err = json.Unmarshal(bodyData, &secretResp) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return &secretResp, nil 99 | } 100 | 101 | func (c *ClientV2) GetStaticSecretDetailsRequest(identifier string) (*http.Request, error) { 102 | if identifier == "" { 103 | return nil, fmt.Errorf("Must specify an Identifier") 104 | } 105 | 106 | path := fmt.Sprintf("secrets/static/%s", identifier) 107 | 108 | secretURL := makeRouterURL(c.config.ApplianceURL, path).String() 109 | 110 | request, err := http.NewRequest( 111 | http.MethodGet, 112 | secretURL, 113 | nil, 114 | ) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | request.Header.Add(v2APIOutgoingHeaderID, v2APIHeader) 120 | 121 | return request, nil 122 | } 123 | 124 | func (c *ClientV2) GetStaticSecretDetails(identifier string) (*StaticSecretResponse, error) { 125 | if !isConjurCloudURL(c.config.ApplianceURL) { 126 | return nil, fmt.Errorf("StaticSecret API %s", NotSupportedInConjurEnterprise) 127 | } 128 | 129 | req, err := c.GetStaticSecretDetailsRequest(identifier) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | resp, err := c.SubmitRequest(req) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | bodyData, err := response.DataResponse(resp) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | secretResp := StaticSecretResponse{} 145 | err = json.Unmarshal(bodyData, &secretResp) 146 | if err != nil { 147 | return nil, err 148 | } 149 | 150 | return &secretResp, nil 151 | } 152 | 153 | func (c *ClientV2) GetStaticSecretPermissionsRequest(identifier string) (*http.Request, error) { 154 | if identifier == "" { 155 | return nil, fmt.Errorf("Must specify an Identifier") 156 | } 157 | 158 | path := fmt.Sprintf("secrets/static/%s/permissions", identifier) 159 | 160 | secretURL := makeRouterURL(c.config.ApplianceURL, path).String() 161 | 162 | request, err := http.NewRequest( 163 | http.MethodGet, 164 | secretURL, 165 | nil, 166 | ) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | request.Header.Add(v2APIOutgoingHeaderID, v2APIHeader) 172 | 173 | return request, nil 174 | } 175 | 176 | func (c *ClientV2) GetStaticSecretPermissions(identifier string) (*PermissionResponse, error) { 177 | if !isConjurCloudURL(c.config.ApplianceURL) { 178 | return nil, fmt.Errorf("StaticSecret API %s", NotSupportedInConjurEnterprise) 179 | } 180 | 181 | req, err := c.GetStaticSecretPermissionsRequest(identifier) 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | resp, err := c.SubmitRequest(req) 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | bodyData, err := response.DataResponse(resp) 192 | if err != nil { 193 | return nil, err 194 | } 195 | 196 | permissionsResp := PermissionResponse{} 197 | err = json.Unmarshal(bodyData, &permissionsResp) 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | return &permissionsResp, nil 203 | } 204 | 205 | func (s StaticSecret) Validate() error { 206 | var errs []error 207 | if s.Branch == "" { 208 | errs = append(errs, fmt.Errorf("Missing required StaticSecret attribute Branch")) 209 | } 210 | if s.Name == "" { 211 | errs = append(errs, fmt.Errorf("Missing required StaticSecret attribute Name")) 212 | } 213 | if len(errs) > 0 { 214 | return errors.Join(errs...) 215 | } 216 | return nil 217 | } 218 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | For general contribution and community guidelines, please see the [community repo](https://github.com/cyberark/community). 4 | 5 | ## Contributing 6 | 7 | 1. [Fork the project](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) 8 | 2. [Clone your fork](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) 9 | 3. Make local changes to your fork by editing files 10 | 3. [Commit your changes](https://help.github.com/en/github/managing-files-in-a-repository/adding-a-file-to-a-repository-using-the-command-line) 11 | 4. [Push your local changes to the remote server](https://help.github.com/en/github/using-git/pushing-commits-to-a-remote-repository) 12 | 5. [Create new Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) 13 | 14 | From here your pull request will be reviewed and once you've responded to all 15 | feedback it will be merged into the project. Congratulations, you're a 16 | contributor! 17 | 18 | ## Development 19 | To start developing and testing using our development scripts , 20 | the following tools need to be installed: 21 | 22 | - Docker 23 | - docker-compose 24 | 25 | ### Running tests 26 | 27 | To run the test suite, run: 28 | ```shell 29 | ./bin/test.sh 30 | ``` 31 | 32 | This will spin up a containerized Conjur OSS environment and build the test containers, 33 | and will run all tests. 34 | 35 | To run the tests against a specific version of Golang, you can run the following: 36 | ```shell 37 | ./bin/test.sh 1.24 38 | ``` 39 | 40 | This will spin up a containerized Conjur OSS environment and build the test containers, 41 | and will run the tests in a `golang:1.24` container 42 | 43 | Supported arguments are `1.24` and `1.25`, with the 44 | default being `1.25` if no argument is given. 45 | 46 | ### Setting up a development environment 47 | To start a container with terminal access, and the necessary 48 | test running dependencies installed, run: 49 | 50 | ```shell 51 | ./bin/dev.sh 52 | ``` 53 | 54 | You can then run the following command from the container terminal to run 55 | all tests: 56 | 57 | ```shell 58 | go test -coverprofile="output/c.out" -v ./... | tee output/junit.output; 59 | exit_code=$?; 60 | echo "Exit code: $exit_code" 61 | ``` 62 | 63 | ### Working with Unreleased Changes 64 | 65 | #### For CI/Jenkins Builds 66 | 1. Jenkins automatically switches dependencies to internal Enterprise versions, allowing CI to build against private repos without requiring public releases. 67 | - *Note:* Changes still need to be merged to `main` in the internal `conjur-api-go` repository before the downstream repositories will be able to use them. 68 | 2. In the downstream project (e.g., conjur-cli-go), add replace statements to the bottom of `go.mod` to ensure that the internal dependencies are pulled in when running the CI pipeline: 69 | ``` 70 | replace github.com/cyberark/conjur-api-go => github.com/cyberark/conjur-api-go latest 71 | ``` 72 | - *Note:* the custom replace statements and CI business logic are specific to CyberArk internal contributors 73 | - See the [secrets provider go.mod](https://github.com/cyberark/secrets-provider-for-k8s/blob/main/go.mod) for examples of proper replace statements 74 | 75 | #### For Local Development 76 | 1. Locally, you need to follow standard Go practice of replacing the dependency in `go.mod ` with the version in a local directory. 77 | - See [Go Documentation: Requiring Module Code in a Local Directory](https://go.dev/doc/modules/managing-dependencies#local_directory) 78 | 79 | 80 | ## Releases 81 | 82 | Releases should be created by maintainers only. To create a tag and release, 83 | follow the instructions in this section. 84 | 85 | ### Update the changelog and notices (if necessary) 86 | 1. Update the `CHANGELOG.md` file with the new version and the changes that are included in the release. 87 | 1. Update `NOTICES.txt` 88 | ```sh-session 89 | go install github.com/google/go-licenses@latest 90 | # Verify that dependencies fit into supported licenses types. 91 | # If there is new dependency having unsupported license, that license should be 92 | # included to notices.tpl file in order to get generated in NOTICES.txt. 93 | $(go env GOPATH)/bin/go-licenses check ./... \ 94 | --allowed_licenses="MIT,ISC,Apache-2.0,BSD-3-Clause,BSD-2-Clause,MPL-2.0" \ 95 | --ignore $(go list std | awk 'NR > 1 { printf(",") } { printf("%s",$0) } END { print "" }') 96 | # If no errors occur, proceed to generate updated NOTICES.txt 97 | $(go env GOPATH)/bin/go-licenses report ./... \ 98 | --template notices.tpl \ 99 | --ignore github.com/cyberark/conjur-api-go \ 100 | --ignore $(go list std | awk 'NR > 1 { printf(",") } { printf("%s",$0) } END { print "" }') \ 101 | > NOTICES.txt 102 | ``` 103 | 104 | ### Pre-requisites 105 | 106 | 1. Review the git log and ensure the [changelog](CHANGELOG.md) contains all 107 | relevant recent changes with references to GitHub issues or PRs, if possible. 108 | Also ensure the latest unreleased version is accurate - our pipeline generates 109 | a VERSION file based on the changelog, which is then used to assign the version 110 | of the release and any release artifacts. 111 | 1. Ensure that all documentation that needs to be written has been 112 | written by TW, approved by PO/Engineer, and pushed to the forward-facing documentation. 113 | 1. Scan the project for vulnerabilities 114 | 115 | ### Release and Promote 116 | 117 | 1. Merging into main/master branches will automatically trigger a release. If successful, this release can be promoted at a later time. 118 | 1. Jenkins build parameters can be utilized to promote a successful release or manually trigger aditional releases as needed. 119 | 1. Reference the [internal automated release doc](https://github.com/conjurinc/docs/blob/master/reference/infrastructure/automated_releases.md#release-and-promotion-process) for releasing and promoting. 120 | -------------------------------------------------------------------------------- /conjurapi/authn/auth_token_test.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestToken_Parse(t *testing.T) { 12 | 13 | token_s := `{"protected":"eyJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiI5M2VjNTEwODRmZTM3Zjc3M2I1ODhlNTYyYWVjZGMxMSJ9","payload":"eyJzdWIiOiJhZG1pbiIsImlhdCI6MTUxMDc1MzI1OX0=","signature":"raCufKOf7sKzciZInQTphu1mBbLhAdIJM72ChLB4m5wKWxFnNz_7LawQ9iYEI_we1-tdZtTXoopn_T1qoTplR9_Bo3KkpI5Hj3DB7SmBpR3CSRTnnEwkJ0_aJ8bql5Cbst4i4rSftyEmUqX-FDOqJdAztdi9BUJyLfbeKTW9OGg-QJQzPX1ucB7IpvTFCEjMoO8KUxZpbHj-KpwqAMZRooG4ULBkxp5nSfs-LN27JupU58oRgIfaWASaDmA98O2x6o88MFpxK_M0FeFGuDKewNGrRc8lCOtTQ9cULA080M5CSnruCqu1Qd52r72KIOAfyzNIiBCLTkblz2fZyEkdSKQmZ8J3AakxQE2jyHmMT-eXjfsEIzEt-IRPJIirI3Qm"}` 14 | token_with_exp_s := `{"protected":"eyJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiI5M2VjNTEwODRmZTM3Zjc3M2I1ODhlNTYyYWVjZGMxMSJ9","payload":"eyJzdWIiOiJhZG1pbiIsImlhdCI6MTUxMDc1MzI1OSwiZXhwIjoxNTEwNzUzMzU5fQo=","signature":"raCufKOf7sKzciZInQTphu1mBbLhAdIJM72ChLB4m5wKWxFnNz_7LawQ9iYEI_we1-tdZtTXoopn_T1qoTplR9_Bo3KkpI5Hj3DB7SmBpR3CSRTnnEwkJ0_aJ8bql5Cbst4i4rSftyEmUqX-FDOqJdAztdi9BUJyLfbeKTW9OGg-QJQzPX1ucB7IpvTFCEjMoO8KUxZpbHj-KpwqAMZRooG4ULBkxp5nSfs-LN27JupU58oRgIfaWASaDmA98O2x6o88MFpxK_M0FeFGuDKewNGrRc8lCOtTQ9cULA080M5CSnruCqu1Qd52r72KIOAfyzNIiBCLTkblz2fZyEkdSKQmZ8J3AakxQE2jyHmMT-eXjfsEIzEt-IRPJIirI3Qm"}` 15 | token_mangled_s := `{"protected":"eyJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiI5M2VjNTEwODRmZTM3Zjc3M2I1ODhlNTYyYWVjZGMxMSJ9","payload":"WIiOiJhZG1","signature":"raCufKOf7sKzciZInQTphu1mBbLhAdIJM72ChLB4m5wKWxFnNz_7LawQ9iYEI_we1-tdZtTXoopn_T1qoTplR9_Bo3KkpI5Hj3DB7SmBpR3CSRTnnEwkJ0_aJ8bql5Cbst4i4rSftyEmUqX-FDOqJdAztdi9BUJyLfbeKTW9OGg-QJQzPX1ucB7IpvTFCEjMoO8KUxZpbHj-KpwqAMZRooG4ULBkxp5nSfs-LN27JupU58oRgIfaWASaDmA98O2x6o88MFpxK_M0FeFGuDKewNGrRc8lCOtTQ9cULA080M5CSnruCqu1Qd52r72KIOAfyzNIiBCLTkblz2fZyEkdSKQmZ8J3AakxQE2jyHmMT-eXjfsEIzEt-IRPJIirI3Qm"}` 16 | token_mangled_2_s := `{"protected":"eyJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiI5M2VjNTEwODRmZTM3Zjc3M2I1ODhlNTYyYWVjZGMxMSJ9","payload":"Zm9vYmFyCg==","signature":"raCufKOf7sKzciZInQTphu1mBbLhAdIJM72ChLB4m5wKWxFnNz_7LawQ9iYEI_we1-tdZtTXoopn_T1qoTplR9_Bo3KkpI5Hj3DB7SmBpR3CSRTnnEwkJ0_aJ8bql5Cbst4i4rSftyEmUqX-FDOqJdAztdi9BUJyLfbeKTW9OGg-QJQzPX1ucB7IpvTFCEjMoO8KUxZpbHj-KpwqAMZRooG4ULBkxp5nSfs-LN27JupU58oRgIfaWASaDmA98O2x6o88MFpxK_M0FeFGuDKewNGrRc8lCOtTQ9cULA080M5CSnruCqu1Qd52r72KIOAfyzNIiBCLTkblz2fZyEkdSKQmZ8J3AakxQE2jyHmMT-eXjfsEIzEt-IRPJIirI3Qm"}` 17 | 18 | t.Run("Token is parsed successfully", func(t *testing.T) { 19 | token, err := NewToken([]byte(token_s)) 20 | 21 | assert.NoError(t, err) 22 | assert.Equal(t, "*authn.AuthnToken", reflect.TypeOf(token).String()) 23 | assert.NotNil(t, token.Raw()) 24 | }) 25 | 26 | t.Run("Token fields are parsed as expected", func(t *testing.T) { 27 | token, err := NewToken([]byte(token_s)) 28 | assert.NoError(t, err) 29 | 30 | assert.Equal(t, token_s, string(token.Raw())) 31 | 32 | assert.Equal(t, time.Unix(1510753259, 0).String(), token.iat.String()) 33 | assert.Nil(t, token.exp) 34 | 35 | assert.True(t, token.ShouldRefresh()) 36 | }) 37 | 38 | t.Run("Token exp is supported", func(t *testing.T) { 39 | token, err := NewToken([]byte(token_with_exp_s)) 40 | assert.NoError(t, err) 41 | 42 | assert.Equal(t, time.Unix(1510753259, 0).String(), token.iat.String()) 43 | assert.Equal(t, time.Unix(1510753359, 0).String(), token.exp.String()) 44 | 45 | assert.True(t, token.ShouldRefresh()) 46 | }) 47 | 48 | t.Run("Malformed base64 in token is reported", func(t *testing.T) { 49 | _, err := NewToken([]byte(token_mangled_s)) 50 | assert.Equal(t, "access token field 'payload' is not valid base64", err.Error()) 51 | }) 52 | 53 | t.Run("Malformed JSON in token is reported", func(t *testing.T) { 54 | _, err := NewToken([]byte(token_mangled_2_s)) 55 | assert.Equal(t, "Unable to unmarshal access token field 'payload': invalid character 'o' in literal false (expecting 'a')", err.Error()) 56 | }) 57 | 58 | t.Run("Invalid JSON in token is reported", func(t *testing.T) { 59 | token, err := NewToken([]byte("invalid json")) 60 | assert.EqualError(t, err, "Unable to unmarshal token: invalid character 'i' looking for beginning of value") 61 | assert.Nil(t, token) 62 | }) 63 | 64 | t.Run("Token without correct fields", func(t *testing.T) { 65 | token, err := NewToken([]byte(`{"foo":"bar"}`)) 66 | assert.EqualError(t, err, "Unrecognized token format") 67 | assert.Nil(t, token) 68 | }) 69 | 70 | t.Run("Token without iat", func(t *testing.T) { 71 | token_without_iat := `{"protected":"eyJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiI5M2VjNTEwODRmZTM3Zjc3M2I1ODhlNTYyYWVjZGMxMSJ9","payload":"eyJzdWIiOiJhZG1pbiJ9Cg==","signature":"raCufKOf7sKzciZInQTphu1mBbLhAdIJM72ChLB4m5wKWxFnNz_7LawQ9iYEI_we1-tdZtTXoopn_T1qoTplR9_Bo3KkpI5Hj3DB7SmBpR3CSRTnnEwkJ0_aJ8bql5Cbst4i4rSftyEmUqX-FDOqJdAztdi9BUJyLfbeKTW9OGg-QJQzPX1ucB7IpvTFCEjMoO8KUxZpbHj-KpwqAMZRooG4ULBkxp5nSfs-LN27JupU58oRgIfaWASaDmA98O2x6o88MFpxK_M0FeFGuDKewNGrRc8lCOtTQ9cULA080M5CSnruCqu1Qd52r72KIOAfyzNIiBCLTkblz2fZyEkdSKQmZ8J3AakxQE2jyHmMT-eXjfsEIzEt-IRPJIirI3Qm"}` 72 | _, err := NewToken([]byte(token_without_iat)) 73 | assert.EqualError(t, err, "access token field 'payload' does not contain 'iat'") 74 | }) 75 | 76 | t.Run("Token expired before issued", func(t *testing.T) { 77 | token_exp_before_issued := `{"protected":"eyJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiI5M2VjNTEwODRmZTM3Zjc3M2I1ODhlNTYyYWVjZGMxMSJ9","payload":"eyJzdWIiOiJhZG1pbiIsImlhdCI6MTUxMDc1MzM1OSwiZXhwIjoxNTEwNzUzMjU5fQo=","signature":"raCufKOf7sKzciZInQTphu1mBbLhAdIJM72ChLB4m5wKWxFnNz_7LawQ9iYEI_we1-tdZtTXoopn_T1qoTplR9_Bo3KkpI5Hj3DB7SmBpR3CSRTnnEwkJ0_aJ8bql5Cbst4i4rSftyEmUqX-FDOqJdAztdi9BUJyLfbeKTW9OGg-QJQzPX1ucB7IpvTFCEjMoO8KUxZpbHj-KpwqAMZRooG4ULBkxp5nSfs-LN27JupU58oRgIfaWASaDmA98O2x6o88MFpxK_M0FeFGuDKewNGrRc8lCOtTQ9cULA080M5CSnruCqu1Qd52r72KIOAfyzNIiBCLTkblz2fZyEkdSKQmZ8J3AakxQE2jyHmMT-eXjfsEIzEt-IRPJIirI3Qm"}` 78 | _, err := NewToken([]byte(token_exp_before_issued)) 79 | assert.EqualError(t, err, "access token expired before it was issued") 80 | }) 81 | 82 | t.Run("FromJSON returns error when provided invalid JSON", func(t *testing.T) { 83 | token := &AuthnToken{} 84 | err := token.FromJSON([]byte("invalid json")) 85 | assert.EqualError(t, err, "Unable to unmarshal access token: invalid character 'i' looking for beginning of value") 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /conjurapi/issuer.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/cyberark/conjur-api-go/conjurapi/response" 10 | ) 11 | 12 | // Issuer defines the JSON data structure used with the Conjur API 13 | type Issuer struct { 14 | ID string `json:"id"` 15 | Type string `json:"type"` 16 | MaxTTL int `json:"max_ttl"` 17 | Data map[string]interface{} `json:"data"` 18 | 19 | // Metadata fields returned by the Conjur API 20 | CreatedAt string `json:"created_at,omitempty"` 21 | ModifiedAt string `json:"modified_at,omitempty"` 22 | } 23 | 24 | // IssuerUpdate defines the specific fields allowed in an Issuer update 25 | // request. 26 | type IssuerUpdate struct { 27 | MaxTTL *int `json:"max_ttl,omitempty"` 28 | Data map[string]interface{} `json:"data,omitempty"` 29 | } 30 | 31 | // IssuerList defines the JSON structure returned by the issuer list endpoint 32 | // in the Conjur API 33 | type IssuerList struct { 34 | Issuers []Issuer `json:"issuers"` 35 | } 36 | 37 | // CreateIssuer creates a new Issuer in Conjur 38 | func (c *Client) CreateIssuer(issuer Issuer) (created Issuer, err error) { 39 | req, err := c.createIssuerRequest(issuer) 40 | if err != nil { 41 | return 42 | } 43 | 44 | resp, err := c.SubmitRequest(req) 45 | if err != nil { 46 | return 47 | } 48 | 49 | data, err := response.DataResponse(resp) 50 | if err != nil { 51 | return 52 | } 53 | 54 | err = json.Unmarshal(data, &created) 55 | return 56 | } 57 | 58 | // DeleteIssuer deletes an existing Issuer in Conjur 59 | func (c *Client) DeleteIssuer(issuerID string, keepSecrets bool) (err error) { 60 | req, err := c.deleteIssuerRequest(issuerID, keepSecrets) 61 | if err != nil { 62 | return 63 | } 64 | 65 | resp, err := c.SubmitRequest(req) 66 | if err != nil { 67 | return 68 | } 69 | 70 | err = response.EmptyResponse(resp) 71 | return 72 | } 73 | 74 | // Issuer retrieves an existing Issuer with the given ID 75 | func (c *Client) Issuer(issuerID string) (issuer Issuer, err error) { 76 | req, err := c.issuerRequest(issuerID) 77 | if err != nil { 78 | return 79 | } 80 | 81 | resp, err := c.SubmitRequest(req) 82 | if err != nil { 83 | return 84 | } 85 | 86 | data, err := response.DataResponse(resp) 87 | if err != nil { 88 | return 89 | } 90 | 91 | err = json.Unmarshal(data, &issuer) 92 | return 93 | } 94 | 95 | // Issuers returns the collection of Issuers the caller is permitted to view 96 | func (c *Client) Issuers() (issuers []Issuer, err error) { 97 | req, err := c.issuersRequest() 98 | if err != nil { 99 | return 100 | } 101 | 102 | resp, err := c.SubmitRequest(req) 103 | if err != nil { 104 | return 105 | } 106 | 107 | data, err := response.DataResponse(resp) 108 | if err != nil { 109 | return 110 | } 111 | 112 | issuerList := IssuerList{} 113 | err = json.Unmarshal(data, &issuerList) 114 | if err != nil { 115 | return 116 | } 117 | 118 | issuers = issuerList.Issuers 119 | return 120 | } 121 | 122 | // UpdateIssuer modifies the TTL and/or data on an existing Issuer 123 | func (c *Client) UpdateIssuer(issuerID string, issuerUpdate IssuerUpdate) (updated Issuer, err error) { 124 | req, err := c.updateIssuerRequest(issuerID, issuerUpdate) 125 | if err != nil { 126 | return 127 | } 128 | 129 | resp, err := c.SubmitRequest(req) 130 | if err != nil { 131 | return 132 | } 133 | 134 | data, err := response.DataResponse(resp) 135 | if err != nil { 136 | return 137 | } 138 | 139 | err = json.Unmarshal(data, &updated) 140 | return 141 | } 142 | 143 | func (c *Client) createIssuerRequest(issuer Issuer) (*http.Request, error) { 144 | issuersURL := makeRouterURL(c.issuersURL(c.config.Account)) 145 | 146 | issuerJSON, err := json.Marshal(issuer) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | req, err := http.NewRequest( 152 | "POST", 153 | issuersURL.String(), 154 | bytes.NewReader(issuerJSON), 155 | ) 156 | if err != nil { 157 | return nil, err 158 | } 159 | req.Header.Add(ConjurSourceHeader, c.GetTelemetryHeader()) 160 | req.Header.Set("Content-Type", "application/json") 161 | 162 | return req, nil 163 | } 164 | 165 | func (c *Client) deleteIssuerRequest(issuerID string, keepSecrets bool) (*http.Request, error) { 166 | issuerURL := makeRouterURL( 167 | c.issuersURL(c.config.Account), 168 | url.QueryEscape(issuerID), 169 | ).withFormattedQuery("keep_secrets=%t", keepSecrets) 170 | 171 | req, err := http.NewRequest("DELETE", issuerURL.String(), nil) 172 | if err != nil { 173 | return nil, err 174 | } 175 | req.Header.Add(ConjurSourceHeader, c.GetTelemetryHeader()) 176 | 177 | return req, nil 178 | } 179 | 180 | func (c *Client) issuerRequest(issuerID string) (*http.Request, error) { 181 | issuerURL := makeRouterURL( 182 | c.issuersURL(c.config.Account), 183 | url.QueryEscape(issuerID), 184 | ) 185 | 186 | req, err := http.NewRequest("GET", issuerURL.String(), nil) 187 | if err != nil { 188 | return nil, err 189 | } 190 | req.Header.Add(ConjurSourceHeader, c.GetTelemetryHeader()) 191 | 192 | return req, nil 193 | } 194 | 195 | func (c *Client) issuersRequest() (*http.Request, error) { 196 | issuerURL := makeRouterURL(c.issuersURL(c.config.Account)) 197 | 198 | req, err := http.NewRequest("GET", issuerURL.String(), nil) 199 | if err != nil { 200 | return nil, err 201 | } 202 | req.Header.Add(ConjurSourceHeader, c.GetTelemetryHeader()) 203 | 204 | return req, nil 205 | } 206 | 207 | func (c *Client) updateIssuerRequest(issuerID string, issuerUpdate IssuerUpdate) (*http.Request, error) { 208 | issuerURL := makeRouterURL( 209 | c.issuersURL(c.config.Account), 210 | url.QueryEscape(issuerID), 211 | ) 212 | 213 | issuerUpdateJSON, err := json.Marshal(issuerUpdate) 214 | if err != nil { 215 | return nil, err 216 | } 217 | 218 | req, err := http.NewRequest( 219 | "PATCH", 220 | issuerURL.String(), 221 | bytes.NewReader(issuerUpdateJSON), 222 | ) 223 | if err != nil { 224 | return nil, err 225 | } 226 | req.Header.Add(ConjurSourceHeader, c.GetTelemetryHeader()) 227 | req.Header.Set("Content-Type", "application/json") 228 | 229 | return req, nil 230 | } 231 | 232 | func (c *Client) issuersURL(account string) string { 233 | return makeRouterURL(c.config.ApplianceURL, "issuers", account).String() 234 | } 235 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= 2 | al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= 3 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 4 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 5 | github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE= 6 | github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= 7 | github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4= 8 | github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8= 9 | github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI= 10 | github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E= 11 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY= 12 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4= 13 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI= 14 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE= 15 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY= 16 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA= 17 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 18 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 19 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= 20 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= 21 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10= 22 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s= 23 | github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No= 24 | github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc= 25 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU= 26 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA= 27 | github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4= 28 | github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0= 29 | github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= 30 | github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 31 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= 32 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= 33 | github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= 34 | github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= 35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 39 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 40 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 41 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 42 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 43 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 44 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 45 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 46 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 47 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 50 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 51 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 52 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 53 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 56 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 57 | github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= 58 | github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= 59 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 60 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 61 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 63 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 64 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 66 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 67 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 68 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 69 | -------------------------------------------------------------------------------- /conjurapi/resource.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/cyberark/conjur-api-go/conjurapi/response" 9 | ) 10 | 11 | // Resource contains information about the Conjur Resource 12 | type Resource struct { 13 | /* 14 | There are two types of resources in conjur: 15 | Roles, which can be given given permissions on other resources and granted other roles, and 16 | Non-Role Resources, which cannot be given given permissions or granted roles. 17 | 18 | Types of Roles: 19 | * Group 20 | * Host 21 | * Layer 22 | * Policy 23 | * User 24 | Types of Non-Role Resources: 25 | * Variable 26 | * Webservice 27 | */ 28 | 29 | // * Fields for all resources 30 | Identifier string `json:"identifier"` 31 | Id string `json:"id"` 32 | Type string `json:"type"` 33 | Owner string `json:"owner"` 34 | Policy string `json:"policy"` 35 | Annotations map[string]string `json:"annotations"` 36 | 37 | // * Field exlusively for roles 38 | Permitted *map[string][]string `json:"permitted,omitempty"` 39 | 40 | // * Fields that we do not put into json for Roles 41 | Permissions *map[string][]string `json:"permissions,omitempty"` 42 | Members *[]string `json:"members,omitempty"` 43 | Memberships *[]string `json:"memberships,omitempty"` 44 | RestrictedTo *[]string `json:"restricted_to,omitempty"` 45 | } 46 | 47 | type ResourceFilter struct { 48 | Kind string 49 | Search string 50 | Limit int 51 | Offset int 52 | Role string 53 | } 54 | 55 | type ResourcesCount struct { 56 | Count int `json:"count"` 57 | } 58 | 59 | // CheckPermission determines whether the authenticated user has a specified privilege 60 | // on a resource. 61 | func (c *Client) CheckPermission(resourceID string, privilege string) (bool, error) { 62 | req, err := c.CheckPermissionRequest(resourceID, privilege) 63 | if err != nil { 64 | return false, err 65 | } 66 | 67 | return c.processPermissionCheck(req) 68 | } 69 | 70 | // CheckPermissionForRole determines whether the provided role has a specific 71 | // privilege on a resource. 72 | func (c *Client) CheckPermissionForRole(resourceID string, roleID string, privilege string) (bool, error) { 73 | req, err := c.CheckPermissionForRoleRequest(resourceID, roleID, privilege) 74 | if err != nil { 75 | return false, err 76 | } 77 | 78 | return c.processPermissionCheck(req) 79 | } 80 | 81 | func (c *Client) processPermissionCheck(req *http.Request) (bool, error) { 82 | resp, err := c.SubmitRequest(req) 83 | if err != nil { 84 | return false, err 85 | } 86 | 87 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 88 | return true, nil 89 | } else if resp.StatusCode == 404 || resp.StatusCode == 403 { 90 | return false, nil 91 | } else { 92 | return false, fmt.Errorf("Permission check failed with HTTP status %d", resp.StatusCode) 93 | } 94 | } 95 | 96 | // ResourceExists checks whether or not a resource exists 97 | func (c *Client) ResourceExists(resourceID string) (bool, error) { 98 | req, err := c.ResourceRequest(resourceID) 99 | if err != nil { 100 | return false, err 101 | } 102 | 103 | resp, err := c.SubmitRequest(req) 104 | if err != nil { 105 | return false, err 106 | } 107 | 108 | if (resp.StatusCode >= 200 && resp.StatusCode < 300) || resp.StatusCode == 403 { 109 | return true, nil 110 | } else if resp.StatusCode == 404 { 111 | return false, nil 112 | } else { 113 | return false, fmt.Errorf("Resource exists check failed with HTTP status %d", resp.StatusCode) 114 | } 115 | } 116 | 117 | // Resource fetches a single user-visible resource by id. 118 | func (c *Client) Resource(resourceID string) (resource map[string]interface{}, err error) { 119 | req, err := c.ResourceRequest(resourceID) 120 | if err != nil { 121 | return 122 | } 123 | 124 | resp, err := c.SubmitRequest(req) 125 | if err != nil { 126 | return 127 | } 128 | 129 | data, err := response.DataResponse(resp) 130 | if err != nil { 131 | return 132 | } 133 | 134 | resource = make(map[string]interface{}) 135 | err = json.Unmarshal(data, &resource) 136 | return 137 | } 138 | 139 | // Resources fetches user-visible resources. The set of resources can 140 | // be limited by the given ResourceFilter. If filter is non-nil, only 141 | // non-zero-valued members of the filter will be applied. 142 | func (c *Client) Resources(filter *ResourceFilter) (resources []map[string]interface{}, err error) { 143 | req, err := c.ResourcesRequest(filter) 144 | if err != nil { 145 | return 146 | } 147 | 148 | resp, err := c.SubmitRequest(req) 149 | if err != nil { 150 | return 151 | } 152 | 153 | data, err := response.DataResponse(resp) 154 | if err != nil { 155 | return 156 | } 157 | 158 | resources = make([]map[string]interface{}, 1) 159 | err = json.Unmarshal(data, &resources) 160 | return 161 | } 162 | 163 | // ResourcesCount counts user-visible resources. The set of resources can 164 | // be limited by the given ResourceFilter. If filter is non-nil, only 165 | // non-zero-valued members of the filter will be applied. 166 | func (c *Client) ResourcesCount(filter *ResourceFilter) (*ResourcesCount, error) { 167 | req, err := c.ResourcesCountRequest(filter) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | resp, err := c.SubmitRequest(req) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | data, err := response.DataResponse(resp) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | resourcesCount := &ResourcesCount{} 183 | err = json.Unmarshal(data, resourcesCount) 184 | return resourcesCount, nil 185 | } 186 | 187 | func (c *Client) ResourceIDs(filter *ResourceFilter) ([]string, error) { 188 | resources, err := c.Resources(filter) 189 | 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | resourceIDs := make([]string, 0) 195 | 196 | for _, element := range resources { 197 | resourceIDs = append(resourceIDs, element["id"].(string)) 198 | } 199 | 200 | return resourceIDs, nil 201 | } 202 | 203 | // PermittedRoles lists the roles which have the named permission on a resource 204 | func (c *Client) PermittedRoles(resourceID, privilege string) ([]string, error) { 205 | req, err := c.PermittedRolesRequest(resourceID, privilege) 206 | if err != nil { 207 | return nil, err 208 | } 209 | 210 | resp, err := c.SubmitRequest(req) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | data, err := response.DataResponse(resp) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | roles := make([]string, 0) 221 | err = json.Unmarshal(data, &roles) 222 | return roles, nil 223 | } 224 | -------------------------------------------------------------------------------- /conjurapi/authenticators.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cyberark/conjur-api-go/conjurapi/response" 7 | ) 8 | 9 | // AuthenticatorStatusResponse contains information about 10 | // the status of an authenticator. 11 | type AuthenticatorStatusResponse struct { 12 | // Status of the policy validation. 13 | Status string `json:"status"` 14 | Error string `json:"error"` 15 | } 16 | 17 | func (c *Client) AuthenticatorStatus(authenticatorType string, serviceID string) (*AuthenticatorStatusResponse, error) { 18 | req, err := c.AuthenticatorStatusRequest(authenticatorType, serviceID) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | res, err := c.SubmitRequest(req) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | obj := AuthenticatorStatusResponse{} 29 | return &obj, response.JSONResponse(res, &obj) 30 | } 31 | 32 | // EnableAuthenticator enables or disables an authenticator instance 33 | // 34 | // The authenticated user must be admin 35 | func (c *Client) EnableAuthenticator(authenticatorType string, serviceID string, enabled bool) error { 36 | req, err := c.EnableAuthenticatorRequest(authenticatorType, serviceID, enabled) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | resp, err := c.SubmitRequest(req) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | err = response.EmptyResponse(resp) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // Bool is a helper function to create a pointer to a boolean value. 55 | func Bool(v bool) *bool { return &v } 56 | 57 | type AuthenticatorBase struct { 58 | Type string `json:"type"` 59 | Subtype *string `json:"subtype,omitempty"` 60 | Name string `json:"name"` 61 | Enabled *bool `json:"enabled,omitempty"` 62 | Owner *AuthOwner `json:"owner,omitempty"` 63 | Data map[string]interface{} `json:"data,omitempty"` 64 | Annotations map[string]string `json:"annotations,omitempty"` 65 | } 66 | 67 | type AuthenticatorResponse struct { 68 | AuthenticatorBase 69 | Branch string `json:"branch"` 70 | } 71 | 72 | type AuthOwner struct { 73 | ID string `json:"id"` 74 | Kind string `json:"kind"` 75 | } 76 | 77 | type AuthenticatorListResponse struct { 78 | Authenticators []AuthenticatorResponse `json:"authenticators"` 79 | Count int `json:"count"` 80 | } 81 | 82 | const AuthenticatorsMinVersion = "1.23.0" 83 | 84 | // CreateAuthenticator creates a new authenticator instance using the V2 API. 85 | // 86 | // The authenticated user must have create privileges on the conjur/authn- policy. 87 | func (c *ClientV2) CreateAuthenticator(authenticator *AuthenticatorBase) (*AuthenticatorResponse, error) { 88 | if !isConjurCloudURL(c.config.ApplianceURL) && c.VerifyMinServerVersion(AuthenticatorsMinVersion) != nil { 89 | return nil, fmt.Errorf("authenticators API is not supported in Conjur versions older than %s", AuthenticatorsMinVersion) 90 | } 91 | 92 | req, err := c.CreateAuthenticatorRequest(authenticator) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | res, err := c.SubmitRequest(req) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | obj := AuthenticatorResponse{} 103 | return &obj, response.JSONResponse(res, &obj) 104 | } 105 | 106 | // GetAuthenticator gets an existing authenticator instance using the V2 API. 107 | // 108 | // The authenticated user must have read privileges on the authenticator. 109 | func (c *ClientV2) GetAuthenticator(authenticatorType string, authenticatorName string) (*AuthenticatorResponse, error) { 110 | if !isConjurCloudURL(c.config.ApplianceURL) && c.VerifyMinServerVersion(AuthenticatorsMinVersion) != nil { 111 | return nil, fmt.Errorf("authenticators API is not supported in Conjur versions older than %s", AuthenticatorsMinVersion) 112 | } 113 | 114 | req, err := c.GetAuthenticatorRequest(authenticatorType, authenticatorName) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | res, err := c.SubmitRequest(req) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | obj := AuthenticatorResponse{} 125 | return &obj, response.JSONResponse(res, &obj) 126 | } 127 | 128 | // UpdateAuthenticator updates an existing authenticator instance using the V2 API. 129 | // It currently only supports enabling/disabling an authenticator. 130 | // 131 | // The authenticated user must have update privileges on the authenticator. 132 | func (c *ClientV2) UpdateAuthenticator(authenticatorType string, authenticatorName string, enabled bool) (*AuthenticatorResponse, error) { 133 | if !isConjurCloudURL(c.config.ApplianceURL) && c.VerifyMinServerVersion(AuthenticatorsMinVersion) != nil { 134 | return nil, fmt.Errorf("authenticators API is not supported in Conjur versions older than %s", AuthenticatorsMinVersion) 135 | } 136 | 137 | req, err := c.UpdateAuthenticatorRequest(authenticatorType, authenticatorName, enabled) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | res, err := c.SubmitRequest(req) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | obj := AuthenticatorResponse{} 148 | return &obj, response.JSONResponse(res, &obj) 149 | } 150 | 151 | // DeleteAuthenticator deletes an existing authenticator instance using the V2 API. 152 | // 153 | // The authenticated user must have update privileges on the authenticator. 154 | func (c *ClientV2) DeleteAuthenticator(authenticatorType string, authenticatorName string) error { 155 | if !isConjurCloudURL(c.config.ApplianceURL) && c.VerifyMinServerVersion(AuthenticatorsMinVersion) != nil { 156 | return fmt.Errorf("authenticators API is not supported in Conjur versions older than %s", AuthenticatorsMinVersion) 157 | } 158 | 159 | req, err := c.DeleteAuthenticatorRequest(authenticatorType, authenticatorName) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | resp, err := c.SubmitRequest(req) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | err = response.EmptyResponse(resp) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | return nil 175 | } 176 | 177 | // ListAuthenticators gets a list of existing authenticators using the V2 API. 178 | // 179 | // The authenticated user must have read privileges on the authenticators. 180 | func (c *ClientV2) ListAuthenticators() (*AuthenticatorListResponse, error) { 181 | if !isConjurCloudURL(c.config.ApplianceURL) && c.VerifyMinServerVersion(AuthenticatorsMinVersion) != nil { 182 | return nil, fmt.Errorf("authenticators API is not supported in Conjur versions older than %s", AuthenticatorsMinVersion) 183 | } 184 | 185 | req, err := c.ListAuthenticatorsRequest() 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | res, err := c.SubmitRequest(req) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | obj := AuthenticatorListResponse{} 196 | return &obj, response.JSONResponse(res, &obj) 197 | } 198 | -------------------------------------------------------------------------------- /conjurapi/secret_static_v2_test.go: -------------------------------------------------------------------------------- 1 | package conjurapi 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestClientV2_CreateStaticSecretRequest(t *testing.T) { 13 | config := GetConfigForTest("localhost") 14 | client, err := NewClientFromJwt(config) 15 | 16 | secret := StaticSecret{} 17 | secret.Name = "Name" 18 | secret.Branch = "Branch" 19 | 20 | request, err := client.V2().CreateStaticSecretRequest(secret) 21 | require.NoError(t, err) 22 | 23 | assert.Equal(t, request.Header.Get(v2APIOutgoingHeaderID), v2APIHeader) 24 | assert.Equal(t, "application/json", request.Header.Get("Content-Type")) 25 | 26 | request, err = client.V2().CreateStaticSecretRequest(secret) 27 | require.NoError(t, err) 28 | 29 | testHost := "localhost/secrets/static" 30 | assert.Equal(t, request.URL.Path, testHost) 31 | 32 | if request.Method != http.MethodPost { 33 | t.Errorf("client.V2.GetStaticSecretDetailsRequest method request is wrong. Expected %s used %s", http.MethodPost, request.Method) 34 | return 35 | } 36 | } 37 | 38 | func TestGetStaticSecretDetailsRequest(t *testing.T) { 39 | ident := "test/ident" 40 | config := GetConfigForTest("localhost") 41 | client, err := NewClientFromJwt(config) 42 | 43 | request, err := client.V2().GetStaticSecretDetailsRequest(ident) 44 | require.NoError(t, err) 45 | 46 | if request == nil { 47 | t.Errorf("client.V2.GetStaticSecretDetailsRequest data returned nil") 48 | } 49 | 50 | assert.Equal(t, request.Header.Get(v2APIOutgoingHeaderID), v2APIHeader) 51 | 52 | testHost := "localhost/secrets/static/" + ident 53 | assert.Equal(t, request.URL.Path, testHost) 54 | 55 | if request.Method != http.MethodGet { 56 | t.Errorf("client.V2.GetStaticSecretDetailsRequest method request is wrong. Expected %s used %s", http.MethodGet, request.Method) 57 | } 58 | } 59 | 60 | func TestGetStaticSecretPermissionsRequest(t *testing.T) { 61 | ident := "test/ident" 62 | config := GetConfigForTest("localhost") 63 | client, err := NewClientFromJwt(config) 64 | 65 | request, err := client.V2().GetStaticSecretPermissionsRequest(ident) 66 | require.NoError(t, err) 67 | 68 | assert.Equal(t, request.Header.Get(v2APIOutgoingHeaderID), v2APIHeader) 69 | 70 | testHost := "localhost/secrets/static/" + ident + "/permissions" 71 | assert.Equal(t, request.URL.Path, testHost) 72 | 73 | if request.Method != http.MethodGet { 74 | t.Errorf("client.V2.GetStaticSecretDetailsRequest method request is wrong. Expected %s used %s", http.MethodGet, request.Method) 75 | } 76 | } 77 | 78 | var staticSecretsTestPolicy = ` 79 | - !host bob 80 | - !group test-users 81 | 82 | - !variable secret 83 | 84 | - !permit 85 | role: !host bob 86 | privilege: [ execute ] 87 | resource: !variable secret 88 | ` 89 | 90 | func TestClientV2_CreateStaticSecret(t *testing.T) { 91 | utils, err := NewTestUtils(&Config{}) 92 | require.NoError(t, err) 93 | _, err = utils.Setup(staticSecretsTestPolicy) 94 | 95 | conjur := utils.Client().V2() 96 | 97 | testCases := []struct { 98 | name string 99 | secret StaticSecret 100 | expectError string 101 | }{ 102 | { 103 | name: "Add static secret missing privileges", 104 | secret: StaticSecret{Branch: "/data/test", Name: "secret2", Permissions: []Permission{{Subject: Subject{Id: "data/test/test-users", Kind: "group"}}}}, 105 | expectError: "privileges", 106 | }, 107 | { 108 | name: "Add static secret", 109 | secret: StaticSecret{Branch: "/data/test", Name: "secret2", MimeType: "application/json", Permissions: []Permission{{Subject: Subject{Id: "data/test/test-users", Kind: "group"}, Privileges: []string{"read"}}}}, 110 | }, 111 | } 112 | 113 | for _, tc := range testCases { 114 | t.Run(tc.name, func(t *testing.T) { 115 | member, err := conjur.CreateStaticSecret(tc.secret) 116 | if isConjurCloudURL(os.Getenv("CONJUR_APPLIANCE_URL")) { 117 | 118 | if tc.expectError != "" { 119 | assert.Error(t, err) 120 | assert.Contains(t, err.Error(), tc.expectError) 121 | } else { 122 | require.NoError(t, err) 123 | assert.Equal(t, tc.secret.Name, member.Name) 124 | assert.Equal(t, tc.secret.Branch, member.Branch) 125 | } 126 | } else { 127 | require.Error(t, err) 128 | require.Contains(t, err.Error(), "is not supported in Conjur Enterprise/OSS") 129 | return 130 | } 131 | }) 132 | } 133 | } 134 | 135 | func TestClientV2_GetStaticSecretDetails(t *testing.T) { 136 | utils, err := NewTestUtils(&Config{}) 137 | require.NoError(t, err) 138 | _, err = utils.Setup(staticSecretsTestPolicy) 139 | 140 | conjur := utils.Client().V2() 141 | 142 | testCases := []struct { 143 | name string 144 | path string 145 | secretName string 146 | secretPath string 147 | secret StaticSecret 148 | expectError string 149 | }{ 150 | { 151 | name: "Get static secret details", 152 | path: "data/test/secret", 153 | secretPath: "/data/test", 154 | secretName: "secret", 155 | }, 156 | } 157 | 158 | for _, tc := range testCases { 159 | t.Run(tc.name, func(t *testing.T) { 160 | member, err := conjur.GetStaticSecretDetails(tc.path) 161 | 162 | if isConjurCloudURL(os.Getenv("CONJUR_APPLIANCE_URL")) { 163 | 164 | if tc.expectError != "" { 165 | assert.Error(t, err) 166 | assert.Contains(t, err.Error(), tc.expectError) 167 | } else { 168 | require.NoError(t, err) 169 | assert.Equal(t, tc.secretName, member.Name) 170 | assert.Equal(t, tc.secretPath, member.Branch) 171 | } 172 | } else { 173 | require.Error(t, err) 174 | require.Contains(t, err.Error(), "is not supported in Conjur Enterprise/OSS") 175 | return 176 | } 177 | }) 178 | } 179 | } 180 | 181 | func TestClientV2_GetStaticSecretPermissions(t *testing.T) { 182 | utils, err := NewTestUtils(&Config{}) 183 | require.NoError(t, err) 184 | _, err = utils.Setup(staticSecretsTestPolicy) 185 | 186 | conjur := utils.Client().V2() 187 | 188 | testCases := []struct { 189 | name string 190 | path string 191 | secret StaticSecret 192 | expectError string 193 | permissions string 194 | }{ 195 | { 196 | name: "Get static secret permissions", 197 | path: "data/test/secret", 198 | permissions: "execute", 199 | }, 200 | } 201 | 202 | for _, tc := range testCases { 203 | t.Run(tc.name, func(t *testing.T) { 204 | member, err := conjur.GetStaticSecretPermissions(tc.path) 205 | if isConjurCloudURL(os.Getenv("CONJUR_APPLIANCE_URL")) { 206 | if tc.expectError != "" { 207 | assert.Error(t, err) 208 | assert.Contains(t, err.Error(), tc.expectError) 209 | } else { 210 | require.NoError(t, err) 211 | if tc.permissions != "" { 212 | assert.Equal(t, member.Permission[0].Privileges[0], tc.permissions) 213 | } 214 | } 215 | } else { 216 | require.Error(t, err) 217 | require.Contains(t, err.Error(), "is not supported in Conjur Enterprise/OSS") 218 | return 219 | } 220 | }) 221 | } 222 | } 223 | --------------------------------------------------------------------------------