Skip to content

Commit

Permalink
Merge pull request #153 from cyberark/check-role-api-branch
Browse files Browse the repository at this point in the history
Implements role parameter in check command in API
  • Loading branch information
john-odonnell authored Jan 26, 2023
2 parents ca59c6a + 4d89632 commit 5da6569
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 41 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
[<account>:]<kind>:<identifier>.
[cyberark/conjur-api-go#153](https://github.com/cyberark/conjur-api-go/pull/153)

## [0.10.2] - 2022-11-14

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion bin/dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bin/start-conjur.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

. ./utils.sh

trap teardown EXIT
trap teardown ERR

announce "Compose Project Name: $COMPOSE_PROJECT_NAME"

Expand Down
2 changes: 2 additions & 0 deletions bin/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"}"

Expand Down
81 changes: 60 additions & 21 deletions conjurapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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",
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -620,15 +647,15 @@ 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
}
return makeRouterURL(c.secretsURL(account), kind, url.PathEscape(id)).String(), nil
}

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
}
Expand Down Expand Up @@ -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 [<account>:]<kind>:<identifier>", id)
}
return tokens[0], tokens[1], tokens[2], nil
}

func NewClient(config Config) (*Client, error) {
Expand Down
18 changes: 17 additions & 1 deletion conjurapi/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package conjurapi
import (
"encoding/json"
"fmt"
"net/http"

"github.com/cyberark/conjur-api-go/conjurapi/response"
)
Expand All @@ -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
Expand Down
87 changes: 70 additions & 17 deletions conjurapi/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,96 @@ 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)
assert.True(t, exists)
}
}

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)
Expand Down

0 comments on commit 5da6569

Please sign in to comment.