Skip to content

Commit

Permalink
Merge pull request #14057 from hashicorp/f-gh-13120-acl-role-rpc-endp…
Browse files Browse the repository at this point in the history
…oints

ACL Role: add RPC, HTTP API, and API SDK functionality.
  • Loading branch information
jrasell committed Aug 11, 2022
2 parents d6a9c14 + 3826b1f commit b8fe43a
Show file tree
Hide file tree
Showing 11 changed files with 1,761 additions and 8 deletions.
130 changes: 130 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"errors"
"fmt"
"time"
)
Expand Down Expand Up @@ -202,6 +203,96 @@ func (a *ACLTokens) ExchangeOneTimeToken(secret string, q *WriteOptions) (*ACLTo
return resp.Token, wm, nil
}

var (
// errMissingACLRoleID is the generic errors to use when a call is missing
// the required ACL Role ID parameter.
errMissingACLRoleID = errors.New("missing ACL role ID")
)

// ACLRoles is used to query the ACL Role endpoints.
type ACLRoles struct {
client *Client
}

// ACLRoles returns a new handle on the ACL roles API client.
func (c *Client) ACLRoles() *ACLRoles {
return &ACLRoles{client: c}
}

// List is used to detail all the ACL roles currently stored within state.
func (a *ACLRoles) List(q *QueryOptions) ([]*ACLRole, *QueryMeta, error) {
var resp []*ACLRole
qm, err := a.client.query("/v1/acl/roles", &resp, q)
if err != nil {
return nil, nil, err
}
return resp, qm, nil
}

// Create is used to create an ACL role.
func (a *ACLRoles) Create(role *ACLRole, w *WriteOptions) (*ACLRole, *WriteMeta, error) {
if role.ID != "" {
return nil, nil, errors.New("cannot specify ACL role ID")
}
var resp ACLRole
wm, err := a.client.write("/v1/acl/role", role, &resp, w)
if err != nil {
return nil, nil, err
}
return &resp, wm, nil
}

// Update is used to update an existing ACL role.
func (a *ACLRoles) Update(role *ACLRole, w *WriteOptions) (*ACLRole, *WriteMeta, error) {
if role.ID == "" {
return nil, nil, errMissingACLRoleID
}
var resp ACLRole
wm, err := a.client.write("/v1/acl/role/"+role.ID, role, &resp, w)
if err != nil {
return nil, nil, err
}
return &resp, wm, nil
}

// Delete is used to delete an ACL role.
func (a *ACLRoles) Delete(roleID string, w *WriteOptions) (*WriteMeta, error) {
if roleID == "" {
return nil, errMissingACLRoleID
}
wm, err := a.client.delete("/v1/acl/role/"+roleID, nil, nil, w)
if err != nil {
return nil, err
}
return wm, nil
}

// Get is used to look up an ACL role.
func (a *ACLRoles) Get(roleID string, q *QueryOptions) (*ACLRole, *QueryMeta, error) {
if roleID == "" {
return nil, nil, errMissingACLRoleID
}
var resp ACLRole
qm, err := a.client.query("/v1/acl/role/"+roleID, &resp, q)
if err != nil {
return nil, nil, err
}
return &resp, qm, nil
}

// GetByName is used to look up an ACL role using its name.
func (a *ACLRoles) GetByName(roleName string, q *QueryOptions) (*ACLRole, *QueryMeta, error) {
if roleName == "" {
return nil, nil, errors.New("missing ACL role name")
}
var resp ACLRole
qm, err := a.client.query("/v1/acl/role/name/"+roleName, &resp, q)
if err != nil {
return nil, nil, err
}
return &resp, qm, nil
}

// ACLPolicyListStub is used to for listing ACL policies
type ACLPolicyListStub struct {
Name string
Expand Down Expand Up @@ -285,3 +376,42 @@ type OneTimeTokenExchangeResponse struct {
type BootstrapRequest struct {
BootstrapSecret string
}

// ACLRole is an abstraction for the ACL system which allows the grouping of
// ACL policies into a single object. ACL tokens can be created and linked to
// a role; the token then inherits all the permissions granted by the policies.
type ACLRole struct {

// ID is an internally generated UUID for this role and is controlled by
// Nomad. It can be used after role creation to update the existing role.
ID string

// Name is unique across the entire set of federated clusters and is
// supplied by the operator on role creation. The name can be modified by
// updating the role and including the Nomad generated ID. This update will
// not affect tokens created and linked to this role. This is a required
// field.
Name string

// Description is a human-readable, operator set description that can
// provide additional context about the role. This is an optional field.
Description string

// Policies is an array of ACL policy links. Although currently policies
// can only be linked using their name, in the future we will want to add
// IDs also and thus allow operators to specify either a name, an ID, or
// both. At least one entry is required.
Policies []*ACLRolePolicyLink

CreateIndex uint64
ModifyIndex uint64
}

// ACLRolePolicyLink is used to link a policy to an ACL role. We use a struct
// rather than a list of strings as in the future we will want to add IDs to
// policies and then link via these.
type ACLRolePolicyLink struct {

// Name is the ACLPolicy.Name value which will be linked to the ACL role.
Name string
}
74 changes: 74 additions & 0 deletions api/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,77 @@ func TestACLTokens_BootstrapValidToken(t *testing.T) {
assertWriteMeta(t, wm)
assert.Equal(t, bootkn, out.SecretID)
}

func TestACLRoles(t *testing.T) {
testutil.Parallel(t)

testClient, testServer, _ := makeACLClient(t, nil, nil)
defer testServer.Stop()

// An initial listing shouldn't return any results.
aclRoleListResp, queryMeta, err := testClient.ACLRoles().List(nil)
require.NoError(t, err)
require.Empty(t, aclRoleListResp)
assertQueryMeta(t, queryMeta)

// Create an ACL policy that can be referenced within the ACL role.
aclPolicy := ACLPolicy{
Name: "acl-role-api-test",
Rules: `namespace "default" {
policy = "read"
}
`,
}
writeMeta, err := testClient.ACLPolicies().Upsert(&aclPolicy, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)

// Create an ACL role referencing the previously created policy.
role := ACLRole{
Name: "acl-role-api-test",
Policies: []*ACLRolePolicyLink{{Name: aclPolicy.Name}},
}
aclRoleCreateResp, writeMeta, err := testClient.ACLRoles().Create(&role, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)
require.NotEmpty(t, aclRoleCreateResp.ID)
require.Equal(t, role.Name, aclRoleCreateResp.Name)

// Another listing should return one result.
aclRoleListResp, queryMeta, err = testClient.ACLRoles().List(nil)
require.NoError(t, err)
require.Len(t, aclRoleListResp, 1)
assertQueryMeta(t, queryMeta)

// Read the role using its ID.
aclRoleReadResp, queryMeta, err := testClient.ACLRoles().Get(aclRoleCreateResp.ID, nil)
require.NoError(t, err)
assertQueryMeta(t, queryMeta)
require.Equal(t, aclRoleCreateResp, aclRoleReadResp)

// Read the role using its name.
aclRoleReadResp, queryMeta, err = testClient.ACLRoles().GetByName(aclRoleCreateResp.Name, nil)
require.NoError(t, err)
assertQueryMeta(t, queryMeta)
require.Equal(t, aclRoleCreateResp, aclRoleReadResp)

// Update the role name.
role.Name = "acl-role-api-test-badger-badger-badger"
role.ID = aclRoleCreateResp.ID
aclRoleUpdateResp, writeMeta, err := testClient.ACLRoles().Update(&role, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)
require.Equal(t, role.Name, aclRoleUpdateResp.Name)
require.Equal(t, role.ID, aclRoleUpdateResp.ID)

// Delete the role.
writeMeta, err = testClient.ACLRoles().Delete(aclRoleCreateResp.ID, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)

// Make sure there are no ACL roles now present.
aclRoleListResp, queryMeta, err = testClient.ACLRoles().List(nil)
require.NoError(t, err)
require.Empty(t, aclRoleListResp)
assertQueryMeta(t, queryMeta)
}
Loading

0 comments on commit b8fe43a

Please sign in to comment.