Skip to content

Commit

Permalink
Implement CheckPermissionForRole
Browse files Browse the repository at this point in the history
  • Loading branch information
john-odonnell committed Jan 25, 2023
1 parent 1846e73 commit 90d060e
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 46 deletions.
58 changes: 34 additions & 24 deletions conjurapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,26 +324,41 @@ func (c *Client) ChangeUserPasswordRequest(username string, password string, new
return req, nil
}

// CheckPermissionRequest crafts an HTTP request to Conjur's /resource endpoint
// to check if a certain user has the given privilege on the given resourceID.
// If the roleID parameter is provided, the check is made on behalf of the
// specified role - otherwise, the check is made of behalf of the authenticated user.
func (c *Client) CheckPermissionRequest(resourceID string, roleID string, privilege string) (*http.Request, error) {
// CheckPermissionForRoleRequest 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
}

query := fmt.Sprintf("check=true&privilege=%s", url.QueryEscape(privilege))
if len(roleID) != 0 {
roleAccount, roleKind, roleIdentifier, err := c.parseID(roleID)
if err != nil {
return nil, err
}

fullyQualifiedRoleID := strings.Join([]string{roleAccount, roleKind, roleIdentifier}, ":")
query = fmt.Sprintf("%s&role=%s", query, url.QueryEscape(fullyQualifiedRoleID))
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(
Expand Down Expand Up @@ -679,24 +694,19 @@ func makeFullId(account, kind, id string) string {
return strings.Join(tokens, ":")
}

// parseID accepts as argument a resource ID, and returns its components - account,
// 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 fully-qualified, the account must match the
// configured account. If the ID is partially-qualified, the configured account
// will be used.
//
// Returns the resource's account, kind, identifier, and fully-qualified ID.
// partially-qualified. If the ID is only partially-qualified, the configured
// account will be returned.
//
// Examples:
// parseID("dev:user:alice") => "dev", "user", "alice", "dev:user:alice", nil
// parseID("user:alice") => "dev", "user", "alice", "dev:user:alice", nil
// parseID("prod:user:alice") => "", "", "", "", error
// 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 {
if tokens[0] != c.config.Account {
return "", "", "", fmt.Errorf("Account of '%s' must match the configured account '%s'", id, c.config.Account)
}
return tokens[0], tokens[1], tokens[2], nil
} else if len(tokens) == 2 {
return c.config.Account, tokens[0], tokens[1], nil
Expand Down
20 changes: 18 additions & 2 deletions 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 string, roleID string, privilege string) (bool, error) {
req, err := c.CheckPermissionRequest(resourceID, roleID, privilege)
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: 68 additions & 19 deletions conjurapi/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,84 @@ import (
"github.com/stretchr/testify/assert"
)

func TestClient_CheckPermission(t *testing.T) {
checkSucceeds := func(conjur *Client, id string, role string) func(t *testing.T) {
return func(t *testing.T) {
allowed, err := conjur.CheckPermission(id, role, "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)
}

checkFails := func(conjur *Client, id string, role string) func(t *testing.T) {
return func(t *testing.T) {
allowed, err := conjur.CheckPermission(id, role, "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 for default admin role", checkSucceeds(conjur, "cucumber:variable:db-password", ""))
t.Run("Check an allowed permission for a role", checkSucceeds(conjur, "cucumber:variable:db-password", "cucumber:user:alice"))
t.Run("Check a permission for an account-less role", checkSucceeds(conjur, "cucumber:variable:db-password", "user:alice"))
t.Run("Check a permission on account-less resource", checkSucceeds(conjur, "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 account-less resource",
checkAndAssert(conjur, assertSuccess, "variable:db-password"),
)
t.Run(
"Check a permission on a non-existent resource",
checkAndAssert(conjur, assertFailure, "cucumber:variable:foobar"),
)
}

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", checkFails(conjur, "cucumber:variable:foobar", "cucumber:user:alice"))
t.Run("Check no permission for a role", checkFails(conjur, "cucumber:variable:db-password", "cucumber:host:bob"))
t.Run(
"Check an allowed permission for a role",
checkAndAssert(conjur, assertSuccess, "cucumber:variable:db-password", "cucumber:user:alice"),
)
t.Run(
"Check a permission for an account-less role",
checkAndAssert(conjur, assertSuccess, "cucumber:variable:db-password", "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", ""),
)
}

func TestClient_ResourceExists(t *testing.T) {
Expand Down
3 changes: 2 additions & 1 deletion conjurapi/variable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ func TestClient_RetrieveSecret(t *testing.T) {
t.Run("Rejects an id from the wrong account", func(t *testing.T) {
_, err := conjur.RetrieveSecret("foobar:variable:" + variableIdentifier)

assert.Equal(t, err.Error(), fmt.Sprintf("Account of 'foobar:variable:%s' must match the configured account 'cucumber'", variableIdentifier))
conjurError := err.(*response.ConjurError)
assert.Equal(t, 404, conjurError.Code)
})

t.Run("Rejects an id with the wrong kind", func(t *testing.T) {
Expand Down

0 comments on commit 90d060e

Please sign in to comment.