Skip to content

Commit

Permalink
Account specifier now optional on resource IDs
Browse files Browse the repository at this point in the history
When an account is specified, it must match the configured account.
When an account is not specified, the configured account is used.
  • Loading branch information
john-odonnell committed Jan 25, 2023
1 parent 161055b commit 9dec532
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 34 deletions.
69 changes: 46 additions & 23 deletions conjurapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,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 @@ -327,18 +324,27 @@ 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) {
account, kind, id, err := parseID(resourceID)
account, kind, id, err := c.parseID(resourceID)
if err != nil {
return nil, err
}

var checkURL string
query := fmt.Sprintf("check=true&privilege=%s", url.QueryEscape(privilege))
if len(roleID) != 0 {
checkURL = makeRouterURL(c.resourcesURL(account), kind, url.QueryEscape(id)).withFormattedQuery("check=true&role=%s&privilege=%s", url.QueryEscape(roleID), url.QueryEscape(privilege)).String()
} else {
checkURL = makeRouterURL(c.resourcesURL(account), kind, url.QueryEscape(id)).withFormattedQuery("check=true&privilege=%s", url.QueryEscape(privilege)).String()
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",
Expand All @@ -348,7 +354,7 @@ func (c *Client) CheckPermissionRequest(resourceID string, roleID string, privil
}

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 @@ -392,7 +398,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 @@ -406,7 +412,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 @@ -420,7 +426,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 @@ -434,7 +440,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 @@ -450,7 +456,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 @@ -606,15 +612,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 @@ -673,13 +679,30 @@ 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 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.
//
// 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
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
} 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
19 changes: 11 additions & 8 deletions conjurapi/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

func TestClient_CheckPermission(t *testing.T) {
checkAllowed := func(conjur *Client, id string, role string) func(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")

Expand All @@ -16,7 +16,7 @@ func TestClient_CheckPermission(t *testing.T) {
}
}

checkNotAllowed := func(conjur *Client, id string, role string) func(t *testing.T) {
checkFails := func(conjur *Client, id string, role string) func(t *testing.T) {
return func(t *testing.T) {
allowed, err := conjur.CheckPermission(id, role, "execute")

Expand All @@ -28,22 +28,25 @@ 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", checkAllowed(conjur, "cucumber:variable:db-password", ""))
t.Run("Check an allowed permission for a role", checkAllowed(conjur, "cucumber:variable:db-password", "cucumber:user:alice"))
t.Run("Check a permission on a non-existent resource", checkNotAllowed(conjur, "cucumber:variable:foobar", "cucumber:user:alice"))
t.Run("Check no permission for a role", checkNotAllowed(conjur, "cucumber:variable:db-password", "cucumber:host:bob"))
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 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"))
}

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
5 changes: 2 additions & 3 deletions conjurapi/variable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestClient_RetrieveSecret(t *testing.T) {
"root",
strings.NewReader(policy),
)

err = conjur.AddSecret(variableIdentifier, oldSecretValue)
assert.NoError(t, err)

Expand Down Expand Up @@ -94,8 +94,7 @@ 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)

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

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

0 comments on commit 9dec532

Please sign in to comment.