Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements role parameter in check command in API #153

Merged
merged 4 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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