diff --git a/CHANGELOG.md b/CHANGELOG.md index fe11d41..260f1e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added support for Conjur's OIDC authenticator [cyberark/conjur-api-go#144](https://github.com/cyberark/conjur-api-go/pull/144) - Added `CONJUR_AUTHN_JWT_TOKEN` to support authenticating via authn-jwt with the contents of a JSON Web Token (JWT) [cyberark/conjur-api-go#143](https://github.com/cyberark/conjur-api-go/pull/140) +- Added new API method `CheckPermissionForRole` + [cyberark/conjur-api-go#153](https://github.com/cyberark/conjur-api-go/pull/153) ### Removed - Remove all usage of Conjur v4 [cyberark/conjur-api-go#139](https://github.com/cyberark/conjur-api-go/pull/139) +### Changed +- Resource IDs can now be partially-qualified, adhering to the form + [:]:. + [cyberark/conjur-api-go#153](https://github.com/cyberark/conjur-api-go/pull/153) + ## [0.10.2] - 2022-11-14 ### Fixed diff --git a/bin/dev.sh b/bin/dev.sh index 0e0a0a8..70deac4 100755 --- a/bin/dev.sh +++ b/bin/dev.sh @@ -6,7 +6,7 @@ cd "$(dirname "$0")" source ./start-conjur.sh docker-compose build dev -docker-compose run --no-deps -d dev +docker-compose up --no-deps -d dev # When we start the dev container, it mounts the top-level directory in # the container. This excludes the vendored dependencies that got diff --git a/bin/start-conjur.sh b/bin/start-conjur.sh index 38c8ada..f345269 100755 --- a/bin/start-conjur.sh +++ b/bin/start-conjur.sh @@ -2,7 +2,7 @@ . ./utils.sh -trap teardown EXIT +trap teardown ERR announce "Compose Project Name: $COMPOSE_PROJECT_NAME" diff --git a/bin/test.sh b/bin/test.sh index e250d2f..90f34ed 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -3,6 +3,8 @@ cd "$(dirname "$0")" . ./utils.sh +trap teardown EXIT + export COMPOSE_PROJECT_NAME="conjurapigo_$(openssl rand -hex 3)" export GO_VERSION="${1:-"1.17"}" diff --git a/conjurapi/client.go b/conjurapi/client.go index b13e0b1..18921d4 100644 --- a/conjurapi/client.go +++ b/conjurapi/client.go @@ -306,13 +306,10 @@ func (c *Client) OidcAuthenticateRequest(code, nonce, code_verifier string) (*ht } func (c *Client) RotateAPIKeyRequest(roleID string) (*http.Request, error) { - account, _, _, err := parseID(roleID) + _, _, _, err := c.parseID(roleID) if err != nil { return nil, err } - if account != c.config.Account { - return nil, fmt.Errorf("Account of '%s' must match the configured account '%s'", roleID, c.config.Account) - } rotateURL := makeRouterURL(c.authnURL(), "api_key").withFormattedQuery("role=%s", roleID).String() @@ -342,12 +339,42 @@ func (c *Client) ChangeUserPasswordRequest(username string, password string, new return req, nil } -func (c *Client) CheckPermissionRequest(resourceID string, privilege string) (*http.Request, error) { - account, kind, id, err := parseID(resourceID) +// CheckPermissionRequest crafts an HTTP request to Conjur's /resource endpoint +// to check if the authenticated user has the given privilege on the given resourceID. +func (c *Client) CheckPermissionRequest(resourceID, privilege string) (*http.Request, error) { + account, kind, id, err := c.parseID(resourceID) if err != nil { return nil, err } - checkURL := makeRouterURL(c.resourcesURL(account), kind, url.QueryEscape(id)).withFormattedQuery("check=true&privilege=%s", url.QueryEscape(privilege)).String() + + query := fmt.Sprintf("check=true&privilege=%s", url.QueryEscape(privilege)) + + checkURL := makeRouterURL(c.resourcesURL(account), kind, url.QueryEscape(id)).withQuery(query).String() + + return http.NewRequest( + "GET", + checkURL, + nil, + ) +} + +// CheckPermissionForRoleRequest crafts an HTTP request to Conjur's /resource endpoint +// to check if a given role has the given privilege on the given resourceID. +func (c *Client) CheckPermissionForRoleRequest(resourceID, roleID, privilege string) (*http.Request, error) { + account, kind, id, err := c.parseID(resourceID) + if err != nil { + return nil, err + } + + roleAccount, roleKind, roleIdentifier, err := c.parseID(roleID) + if err != nil { + return nil, err + } + fullyQualifiedRoleID := strings.Join([]string{roleAccount, roleKind, roleIdentifier}, ":") + + query := fmt.Sprintf("check=true&privilege=%s&role=%s", url.QueryEscape(privilege), url.QueryEscape(fullyQualifiedRoleID)) + + checkURL := makeRouterURL(c.resourcesURL(account), kind, url.QueryEscape(id)).withQuery(query).String() return http.NewRequest( "GET", @@ -357,7 +384,7 @@ func (c *Client) CheckPermissionRequest(resourceID string, privilege string) (*h } func (c *Client) ResourceRequest(resourceID string) (*http.Request, error) { - account, kind, id, err := parseID(resourceID) + account, kind, id, err := c.parseID(resourceID) if err != nil { return nil, err } @@ -401,7 +428,7 @@ func (c *Client) ResourcesRequest(filter *ResourceFilter) (*http.Request, error) } func (c *Client) PermittedRolesRequest(resourceID string, privilege string) (*http.Request, error) { - account, kind, id, err := parseID(resourceID) + account, kind, id, err := c.parseID(resourceID) if err != nil { return nil, err } @@ -415,7 +442,7 @@ func (c *Client) PermittedRolesRequest(resourceID string, privilege string) (*ht } func (c *Client) RoleRequest(roleID string) (*http.Request, error) { - account, kind, id, err := parseID(roleID) + account, kind, id, err := c.parseID(roleID) if err != nil { return nil, err } @@ -429,7 +456,7 @@ func (c *Client) RoleRequest(roleID string) (*http.Request, error) { } func (c *Client) RoleMembersRequest(roleID string) (*http.Request, error) { - account, kind, id, err := parseID(roleID) + account, kind, id, err := c.parseID(roleID) if err != nil { return nil, err } @@ -443,7 +470,7 @@ func (c *Client) RoleMembersRequest(roleID string) (*http.Request, error) { } func (c *Client) RoleMembershipsRequest(roleID string) (*http.Request, error) { - account, kind, id, err := parseID(roleID) + account, kind, id, err := c.parseID(roleID) if err != nil { return nil, err } @@ -459,7 +486,7 @@ func (c *Client) RoleMembershipsRequest(roleID string) (*http.Request, error) { func (c *Client) LoadPolicyRequest(mode PolicyMode, policyID string, policy io.Reader) (*http.Request, error) { fullPolicyID := makeFullId(c.config.Account, "policy", policyID) - account, kind, id, err := parseID(fullPolicyID) + account, kind, id, err := c.parseID(fullPolicyID) if err != nil { return nil, err } @@ -620,7 +647,7 @@ func (c *Client) createHostURL() string { } func (c *Client) variableURL(variableID string) (string, error) { - account, kind, id, err := parseID(variableID) + account, kind, id, err := c.parseID(variableID) if err != nil { return "", err } @@ -628,7 +655,7 @@ func (c *Client) variableURL(variableID string) (string, error) { } func (c *Client) variableWithVersionURL(variableID string, version int) (string, error) { - account, kind, id, err := parseID(variableID) + account, kind, id, err := c.parseID(variableID) if err != nil { return "", err } @@ -687,13 +714,25 @@ func makeFullId(account, kind, id string) string { return strings.Join(tokens, ":") } -func parseID(fullID string) (account, kind, id string, err error) { - tokens := strings.SplitN(fullID, ":", 3) - if len(tokens) != 3 { - err = fmt.Errorf("Id '%s' must be fully qualified", fullID) - return +// parseID accepts as argument a resource ID and returns its components - account, +// resource kind, and identifier. The provided ID can either be fully- or +// partially-qualified. If the ID is only partially-qualified, the configured +// account will be returned. +// +// Examples: +// c.parseID("dev:user:alice") => "dev", "user", "alice", nil +// c.parseID("user:alice") => "dev", "user", "alice", nil +// c.parseID("prod:user:alice") => "prod", "user", "alice", nil +// c.parseID("malformed") => "", "", "". error +func (c *Client) parseID(id string) (account, kind, identifier string, err error) { + tokens := strings.SplitN(id, ":", 3) + if len(tokens) == 3 { + return tokens[0], tokens[1], tokens[2], nil + } else if len(tokens) == 2 { + return c.config.Account, tokens[0], tokens[1], nil + } else { + return "", "", "", fmt.Errorf("Malformed ID '%s': must be fully- or partially-qualified, of form [:]:", id) } - return tokens[0], tokens[1], tokens[2], nil } func NewClient(config Config) (*Client, error) { diff --git a/conjurapi/resource.go b/conjurapi/resource.go index 2ec8796..8de7d0e 100644 --- a/conjurapi/resource.go +++ b/conjurapi/resource.go @@ -3,6 +3,7 @@ package conjurapi import ( "encoding/json" "fmt" + "net/http" "github.com/cyberark/conjur-api-go/conjurapi/response" ) @@ -16,12 +17,27 @@ type ResourceFilter struct { // CheckPermission determines whether the authenticated user has a specified privilege // on a resource. -func (c *Client) CheckPermission(resourceID, privilege string) (bool, error) { +func (c *Client) CheckPermission(resourceID string, privilege string) (bool, error) { req, err := c.CheckPermissionRequest(resourceID, privilege) if err != nil { return false, err } + return c.processPermissionCheck(req) +} + +// CheckPermissionForRole determines whether the provided role has a specific +// privilege on a resource. +func (c *Client) CheckPermissionForRole(resourceID string, roleID string, privilege string) (bool, error) { + req, err := c.CheckPermissionForRoleRequest(resourceID, roleID, privilege) + if err != nil { + return false, err + } + + return c.processPermissionCheck(req) +} + +func (c *Client) processPermissionCheck(req *http.Request) (bool, error) { resp, err := c.SubmitRequest(req) if err != nil { return false, err diff --git a/conjurapi/resource_test.go b/conjurapi/resource_test.go index 8b16c82..08dbf45 100644 --- a/conjurapi/resource_test.go +++ b/conjurapi/resource_test.go @@ -6,35 +6,88 @@ import ( "github.com/stretchr/testify/assert" ) -func TestClient_CheckPermission(t *testing.T) { - checkAllowed := func(conjur *Client, id string) func(t *testing.T) { - return func(t *testing.T) { - allowed, err := conjur.CheckPermission(id, "execute") +type checkAssertion func(t *testing.T, result bool, err error) - assert.NoError(t, err) - assert.True(t, allowed) - } - } +func assertSuccess(t *testing.T, result bool, err error) { + assert.True(t, result) + assert.NoError(t, err) +} - checkNonExisting := func(conjur *Client, id string) func(t *testing.T) { - return func(t *testing.T) { - allowed, err := conjur.CheckPermission(id, "execute") +func assertFailure(t *testing.T, result bool, err error) { + assert.False(t, result) + assert.NoError(t, err) +} - assert.NoError(t, err) - assert.False(t, allowed) +func assertError(t *testing.T, result bool, err error) { + assert.False(t, result) + assert.Error(t, err) +} + +func checkAndAssert( + conjur *Client, + assertion checkAssertion, + args ...string, +) func(t *testing.T) { + return func(t *testing.T) { + var result bool + var err error + + if len(args) == 1 { + result, err = conjur.CheckPermission(args[0], "execute") + } else if len(args) == 2 { + result, err = conjur.CheckPermissionForRole(args[0], args[1], "execute") } + + assertion(t, result, err) } +} +func TestClient_CheckPermission(t *testing.T) { conjur, err := conjurSetup(&Config{}, defaultTestPolicy) assert.NoError(t, err) - t.Run("Check an allowed permission", checkAllowed(conjur, "cucumber:variable:db-password")) + t.Run( + "Check an allowed permission for default role", + checkAndAssert(conjur, assertSuccess, "cucumber:variable:db-password"), + ) + t.Run( + "Check a permission on a non-existent resource", + checkAndAssert(conjur, assertFailure, "cucumber:variable:foobar"), + ) + t.Run( + "Check a permission on account-less resource", + checkAndAssert(conjur, assertSuccess, "variable:db-password"), + ) +} + +func TestClient_CheckPermissionForRole(t *testing.T) { + conjur, err := conjurSetup(&Config{}, defaultTestPolicy) + assert.NoError(t, err) - t.Run("Check a permission on a non-existent resource", checkNonExisting(conjur, "cucumber:variable:foobar")) + t.Run( + "Check an allowed permission for a role", + checkAndAssert(conjur, assertSuccess, "cucumber:variable:db-password", "cucumber:user:alice"), + ) + t.Run( + "Check a permission on a non-existent resource", + checkAndAssert(conjur, assertFailure, "cucumber:variable:foobar", "cucumber:user:alice"), + ) + t.Run( + "Check no permission for a role", + checkAndAssert(conjur, assertFailure, "cucumber:variable:db-password", "cucumber:host:bob"), + ) + t.Run( + "Check a permission with empty role", + checkAndAssert(conjur, assertError, "cucumber:variable:db-password", ""), + ) + t.Run( + "Check a permission for account-less role", + checkAndAssert(conjur, assertSuccess, "variable:db-password", "user:alice"), + ) } func TestClient_ResourceExists(t *testing.T) { - resourceExistent := func(conjur *Client, id string) func (t *testing.T) { + resourceExistent := func(conjur *Client, id string) func(t *testing.T) { return func(t *testing.T) { exists, err := conjur.ResourceExists(id) assert.NoError(t, err) @@ -42,7 +95,7 @@ func TestClient_ResourceExists(t *testing.T) { } } - resourceNonexistent := func(conjur *Client, id string) func (t *testing.T) { + resourceNonexistent := func(conjur *Client, id string) func(t *testing.T) { return func(t *testing.T) { exists, err := conjur.ResourceExists(id) assert.NoError(t, err)