diff --git a/conjurapi/client.go b/conjurapi/client.go index 08d5bb9..adbfdc0 100644 --- a/conjurapi/client.go +++ b/conjurapi/client.go @@ -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() @@ -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", @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -606,7 +612,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 } @@ -614,7 +620,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 } @@ -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 [:]:", id) } - return tokens[0], tokens[1], tokens[2], nil } func NewClient(config Config) (*Client, error) { diff --git a/conjurapi/resource_test.go b/conjurapi/resource_test.go index 5be051d..3aabc08 100644 --- a/conjurapi/resource_test.go +++ b/conjurapi/resource_test.go @@ -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") @@ -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") @@ -28,14 +28,17 @@ 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) @@ -43,7 +46,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) diff --git a/conjurapi/variable_test.go b/conjurapi/variable_test.go index 720e418..9ace95b 100644 --- a/conjurapi/variable_test.go +++ b/conjurapi/variable_test.go @@ -37,7 +37,7 @@ func TestClient_RetrieveSecret(t *testing.T) { "root", strings.NewReader(policy), ) - + err = conjur.AddSecret(variableIdentifier, oldSecretValue) assert.NoError(t, err) @@ -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) {