Support operating on entities and groups by their names (#5355)
* Support operating on entities and groups by their names * address review feedback
This commit is contained in:
parent
b427a23bbb
commit
68a496dde4
|
@ -17,6 +17,35 @@ import (
|
|||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func entityPathFields() map[string]*framework.FieldSchema {
|
||||
return map[string]*framework.FieldSchema{
|
||||
"id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "ID of the entity. If set, updates the corresponding existing entity.",
|
||||
},
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the entity",
|
||||
},
|
||||
"metadata": {
|
||||
Type: framework.TypeKVPairs,
|
||||
Description: `Metadata to be associated with the entity.
|
||||
In CLI, this parameter can be repeated multiple times, and it all gets merged together.
|
||||
For example:
|
||||
vault <command> <path> metadata=key1=value1 metadata=key2=value2
|
||||
`,
|
||||
},
|
||||
"policies": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Policies to be tied to the entity.",
|
||||
},
|
||||
"disabled": {
|
||||
Type: framework.TypeBool,
|
||||
Description: "If set true, tokens tied to this identity will not be able to be used (but will not be revoked).",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// entityPaths returns the API endpoints supported to operate on entities.
|
||||
// Following are the paths supported:
|
||||
// entity - To register a new entity
|
||||
|
@ -26,32 +55,7 @@ func entityPaths(i *IdentityStore) []*framework.Path {
|
|||
return []*framework.Path{
|
||||
{
|
||||
Pattern: "entity$",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "ID of the entity. If set, updates the corresponding existing entity.",
|
||||
},
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the entity",
|
||||
},
|
||||
"metadata": {
|
||||
Type: framework.TypeKVPairs,
|
||||
Description: `Metadata to be associated with the entity.
|
||||
In CLI, this parameter can be repeated multiple times, and it all gets merged together.
|
||||
For example:
|
||||
vault <command> <path> metadata=key1=value1 metadata=key2=value2
|
||||
`,
|
||||
},
|
||||
"policies": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Policies to be tied to the entity.",
|
||||
},
|
||||
"disabled": {
|
||||
Type: framework.TypeBool,
|
||||
Description: "If set true, tokens tied to this identity will not be able to be used (but will not be revoked).",
|
||||
},
|
||||
},
|
||||
Fields: entityPathFields(),
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.UpdateOperation: i.handleEntityUpdateCommon(),
|
||||
},
|
||||
|
@ -60,33 +64,20 @@ vault <command> <path> metadata=key1=value1 metadata=key2=value2
|
|||
HelpDescription: strings.TrimSpace(entityHelp["entity"][1]),
|
||||
},
|
||||
{
|
||||
Pattern: "entity/id/" + framework.GenericNameRegex("id"),
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "ID of the entity.",
|
||||
},
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the entity.",
|
||||
},
|
||||
"metadata": {
|
||||
Type: framework.TypeKVPairs,
|
||||
Description: `Metadata to be associated with the entity.
|
||||
In CLI, this parameter can be repeated multiple times, and it all gets merged together.
|
||||
For example:
|
||||
vault <command> <path> metadata=key1=value1 metadata=key2=value2
|
||||
`,
|
||||
},
|
||||
"policies": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Policies to be tied to the entity.",
|
||||
},
|
||||
"disabled": {
|
||||
Type: framework.TypeBool,
|
||||
Description: "If set true, tokens tied to this identity will not be able to be used (but will not be revoked).",
|
||||
},
|
||||
Pattern: "entity/name/" + framework.GenericNameRegex("name"),
|
||||
Fields: entityPathFields(),
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.UpdateOperation: i.handleEntityUpdateCommon(),
|
||||
logical.ReadOperation: i.pathEntityNameRead(),
|
||||
logical.DeleteOperation: i.pathEntityNameDelete(),
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(entityHelp["entity-name"][0]),
|
||||
HelpDescription: strings.TrimSpace(entityHelp["entity-name"][1]),
|
||||
},
|
||||
{
|
||||
Pattern: "entity/id/" + framework.GenericNameRegex("id"),
|
||||
Fields: entityPathFields(),
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.UpdateOperation: i.handleEntityUpdateCommon(),
|
||||
logical.ReadOperation: i.pathEntityIDRead(),
|
||||
|
@ -96,6 +87,15 @@ vault <command> <path> metadata=key1=value1 metadata=key2=value2
|
|||
HelpSynopsis: strings.TrimSpace(entityHelp["entity-id"][0]),
|
||||
HelpDescription: strings.TrimSpace(entityHelp["entity-id"][1]),
|
||||
},
|
||||
{
|
||||
Pattern: "entity/name/?$",
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ListOperation: i.pathEntityNameList(),
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(entityHelp["entity-name-list"][0]),
|
||||
HelpDescription: strings.TrimSpace(entityHelp["entity-name-list"][1]),
|
||||
},
|
||||
{
|
||||
Pattern: "entity/id/?$",
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
|
@ -202,9 +202,9 @@ func (i *IdentityStore) handleEntityUpdateCommon() framework.OperationFunc {
|
|||
case entityByName == nil:
|
||||
// Not found, safe to use this name with an existing or new entity
|
||||
case entity.ID == "":
|
||||
// We found an entity by name, but we don't currently allow
|
||||
// updating based on name, only ID, so return an error
|
||||
return logical.ErrorResponse("updating entity by name is not currently supported"), nil
|
||||
// Entity by ID was not found, but and entity for the supplied
|
||||
// name was found. Continue updating the entity.
|
||||
entity = entityByName
|
||||
case entity.ID == entityByName.ID:
|
||||
// Same exact entity, carry on (this is basically a noop then)
|
||||
default:
|
||||
|
@ -239,12 +239,26 @@ func (i *IdentityStore) handleEntityUpdateCommon() framework.OperationFunc {
|
|||
if ok {
|
||||
entity.Metadata = metadata.(map[string]string)
|
||||
}
|
||||
|
||||
// At this point, if entity.ID is empty, it indicates that a new entity
|
||||
// is being created. Using this to respond data in the response.
|
||||
newEntity := entity.ID == ""
|
||||
|
||||
// ID creation and some validations
|
||||
err = i.sanitizeEntity(ctx, entity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := i.upsertEntity(ctx, entity, nil, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If this operation was an update to an existing entity, return 204
|
||||
if !newEntity {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Prepare the response
|
||||
respData := map[string]interface{}{
|
||||
"id": entity.ID,
|
||||
|
@ -257,12 +271,6 @@ func (i *IdentityStore) handleEntityUpdateCommon() framework.OperationFunc {
|
|||
|
||||
respData["aliases"] = aliasIDs
|
||||
|
||||
// Update MemDB and persist entity object. New entities have not been
|
||||
// looked up yet so we need to take the lock on the entity on upsert
|
||||
if err := i.upsertEntity(ctx, entity, nil, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return ID of the entity that was either created or updated along with
|
||||
// its aliases
|
||||
return &logical.Response{
|
||||
|
@ -271,6 +279,26 @@ func (i *IdentityStore) handleEntityUpdateCommon() framework.OperationFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// pathEntityNameRead returns the properties of an entity for a given entity ID
|
||||
func (i *IdentityStore) pathEntityNameRead() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
entityName := d.Get("name").(string)
|
||||
if entityName == "" {
|
||||
return logical.ErrorResponse("missing entity name"), nil
|
||||
}
|
||||
|
||||
entity, err := i.MemDBEntityByName(ctx, entityName, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entity == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return i.handleEntityReadCommon(ctx, entity)
|
||||
}
|
||||
}
|
||||
|
||||
// pathEntityIDRead returns the properties of an entity for a given entity ID
|
||||
func (i *IdentityStore) pathEntityIDRead() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
|
@ -394,7 +422,7 @@ func (i *IdentityStore) pathEntityIDDelete() framework.OperationFunc {
|
|||
return nil, err
|
||||
}
|
||||
if entity.NamespaceID != ns.ID {
|
||||
return nil, logical.ErrUnsupportedPath
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Delete all the aliases in the entity. This function will also remove
|
||||
|
@ -423,78 +451,151 @@ func (i *IdentityStore) pathEntityIDDelete() framework.OperationFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// pathEntityIDList lists the IDs of all the valid entities in the identity
|
||||
// store
|
||||
func (i *IdentityStore) pathEntityIDList() framework.OperationFunc {
|
||||
// pathEntityNameDelete deletes the entity for a given entity ID
|
||||
func (i *IdentityStore) pathEntityNameDelete() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
entityName := d.Get("name").(string)
|
||||
if entityName == "" {
|
||||
return logical.ErrorResponse("missing entity name"), nil
|
||||
}
|
||||
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
// Create a MemDB transaction to delete entity
|
||||
txn := i.db.Txn(true)
|
||||
defer txn.Abort()
|
||||
|
||||
// Fetch the entity using its name
|
||||
entity, err := i.MemDBEntityByNameInTxn(txn, ctx, entityName, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If there is no entity for the ID, do nothing
|
||||
if entity == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entity.NamespaceID != ns.ID {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ws := memdb.NewWatchSet()
|
||||
|
||||
txn := i.db.Txn(false)
|
||||
|
||||
iter, err := txn.Get(entitiesTable, "namespace_id", ns.ID)
|
||||
// Delete all the aliases in the entity. This function will also remove
|
||||
// the corresponding alias indexes too.
|
||||
err = i.deleteAliasesInEntityInTxn(txn, entity, entity.Aliases)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed to fetch iterator for entities in memdb: {{err}}", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ws.Add(iter.WatchCh())
|
||||
|
||||
var entityIDs []string
|
||||
entityInfo := map[string]interface{}{}
|
||||
|
||||
type mountInfo struct {
|
||||
MountType string
|
||||
MountPath string
|
||||
// Delete the entity using the same transaction
|
||||
err = i.MemDBDeleteEntityByIDInTxn(txn, entity.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mountAccessorMap := map[string]mountInfo{}
|
||||
|
||||
for {
|
||||
raw := iter.Next()
|
||||
if raw == nil {
|
||||
break
|
||||
}
|
||||
entity := raw.(*identity.Entity)
|
||||
entityIDs = append(entityIDs, entity.ID)
|
||||
entityInfoEntry := map[string]interface{}{
|
||||
"name": entity.Name,
|
||||
}
|
||||
if len(entity.Aliases) > 0 {
|
||||
aliasList := make([]interface{}, 0, len(entity.Aliases))
|
||||
for _, alias := range entity.Aliases {
|
||||
entry := map[string]interface{}{
|
||||
"id": alias.ID,
|
||||
"name": alias.Name,
|
||||
"mount_accessor": alias.MountAccessor,
|
||||
}
|
||||
// Delete the entity from storage
|
||||
err = i.entityPacker.DeleteItem(entity.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mi, ok := mountAccessorMap[alias.MountAccessor]
|
||||
if ok {
|
||||
// Committing the transaction *after* successfully deleting entity
|
||||
txn.Commit()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IdentityStore) pathEntityIDList() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
return i.handlePathEntityListCommon(ctx, req, d, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IdentityStore) pathEntityNameList() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
return i.handlePathEntityListCommon(ctx, req, d, false)
|
||||
}
|
||||
}
|
||||
|
||||
// handlePathEntityListCommon lists the IDs or names of all the valid entities
|
||||
// in the identity store
|
||||
func (i *IdentityStore) handlePathEntityListCommon(ctx context.Context, req *logical.Request, d *framework.FieldData, byID bool) (*logical.Response, error) {
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ws := memdb.NewWatchSet()
|
||||
|
||||
txn := i.db.Txn(false)
|
||||
|
||||
iter, err := txn.Get(entitiesTable, "namespace_id", ns.ID)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed to fetch iterator for entities in memdb: {{err}}", err)
|
||||
}
|
||||
|
||||
ws.Add(iter.WatchCh())
|
||||
|
||||
var keys []string
|
||||
entityInfo := map[string]interface{}{}
|
||||
|
||||
type mountInfo struct {
|
||||
MountType string
|
||||
MountPath string
|
||||
}
|
||||
mountAccessorMap := map[string]mountInfo{}
|
||||
|
||||
for {
|
||||
raw := iter.Next()
|
||||
if raw == nil {
|
||||
break
|
||||
}
|
||||
entity := raw.(*identity.Entity)
|
||||
if byID {
|
||||
keys = append(keys, entity.ID)
|
||||
} else {
|
||||
keys = append(keys, entity.Name)
|
||||
}
|
||||
entityInfoEntry := map[string]interface{}{
|
||||
"name": entity.Name,
|
||||
}
|
||||
if len(entity.Aliases) > 0 {
|
||||
aliasList := make([]interface{}, 0, len(entity.Aliases))
|
||||
for _, alias := range entity.Aliases {
|
||||
entry := map[string]interface{}{
|
||||
"id": alias.ID,
|
||||
"name": alias.Name,
|
||||
"mount_accessor": alias.MountAccessor,
|
||||
}
|
||||
|
||||
mi, ok := mountAccessorMap[alias.MountAccessor]
|
||||
if ok {
|
||||
entry["mount_type"] = mi.MountType
|
||||
entry["mount_path"] = mi.MountPath
|
||||
} else {
|
||||
mi = mountInfo{}
|
||||
if mountValidationResp := i.core.router.validateMountByAccessor(alias.MountAccessor); mountValidationResp != nil {
|
||||
mi.MountType = mountValidationResp.MountType
|
||||
mi.MountPath = mountValidationResp.MountPath
|
||||
entry["mount_type"] = mi.MountType
|
||||
entry["mount_path"] = mi.MountPath
|
||||
} else {
|
||||
mi = mountInfo{}
|
||||
if mountValidationResp := i.core.router.validateMountByAccessor(alias.MountAccessor); mountValidationResp != nil {
|
||||
mi.MountType = mountValidationResp.MountType
|
||||
mi.MountPath = mountValidationResp.MountPath
|
||||
entry["mount_type"] = mi.MountType
|
||||
entry["mount_path"] = mi.MountPath
|
||||
}
|
||||
mountAccessorMap[alias.MountAccessor] = mi
|
||||
}
|
||||
|
||||
aliasList = append(aliasList, entry)
|
||||
mountAccessorMap[alias.MountAccessor] = mi
|
||||
}
|
||||
entityInfoEntry["aliases"] = aliasList
|
||||
}
|
||||
entityInfo[entity.ID] = entityInfoEntry
|
||||
}
|
||||
|
||||
return logical.ListResponseWithInfo(entityIDs, entityInfo), nil
|
||||
aliasList = append(aliasList, entry)
|
||||
}
|
||||
entityInfoEntry["aliases"] = aliasList
|
||||
}
|
||||
entityInfo[entity.ID] = entityInfoEntry
|
||||
}
|
||||
|
||||
return logical.ListResponseWithInfo(keys, entityInfo), nil
|
||||
}
|
||||
|
||||
func (i *IdentityStore) mergeEntity(ctx context.Context, txn *memdb.Txn, toEntity *identity.Entity, fromEntityIDs []string, force, grabLock, mergePolicies bool) (error, error) {
|
||||
|
@ -637,10 +738,18 @@ var entityHelp = map[string][2]string{
|
|||
"Update, read or delete an entity using entity ID",
|
||||
"",
|
||||
},
|
||||
"entity-name": {
|
||||
"Update, read or delete an entity using entity name",
|
||||
"",
|
||||
},
|
||||
"entity-id-list": {
|
||||
"List all the entity IDs",
|
||||
"",
|
||||
},
|
||||
"entity-name-list": {
|
||||
"List all the entity names",
|
||||
"",
|
||||
},
|
||||
"entity-merge-id": {
|
||||
"Merge two or more entities together",
|
||||
"",
|
||||
|
|
|
@ -14,6 +14,122 @@ import (
|
|||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
||||
func TestIdentityStore_EntityByName(t *testing.T) {
|
||||
ctx := namespace.RootContext(nil)
|
||||
i, _, _ := testIdentityStoreWithGithubAuth(ctx, t)
|
||||
|
||||
// Create an entity using the "name" endpoint
|
||||
resp, err := i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "entity/name/testentityname",
|
||||
Operation: logical.UpdateOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatalf("expected a non-nil response")
|
||||
}
|
||||
|
||||
// Test the read by name endpoint
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "entity/name/testentityname",
|
||||
Operation: logical.ReadOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp == nil || resp.Data["name"].(string) != "testentityname" {
|
||||
t.Fatalf("bad entity response: %#v", resp)
|
||||
}
|
||||
|
||||
// Update entity metadata using the name endpoint
|
||||
entityMetadata := map[string]string{
|
||||
"foo": "bar",
|
||||
}
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "entity/name/testentityname",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"metadata": entityMetadata,
|
||||
},
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
|
||||
// Check the updated result
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "entity/name/testentityname",
|
||||
Operation: logical.ReadOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp == nil || !reflect.DeepEqual(resp.Data["metadata"].(map[string]string), entityMetadata) {
|
||||
t.Fatalf("bad entity response: %#v", resp)
|
||||
}
|
||||
|
||||
// Delete the entity using the name endpoint
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "entity/name/testentityname",
|
||||
Operation: logical.DeleteOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
|
||||
// Check if deletion was successful
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "entity/name/testentityname",
|
||||
Operation: logical.ReadOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp != nil {
|
||||
t.Fatalf("expected a nil response")
|
||||
}
|
||||
|
||||
// Create 2 entities
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "entity/name/testentityname",
|
||||
Operation: logical.UpdateOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatalf("expected a non-nil response")
|
||||
}
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "entity/name/testentityname2",
|
||||
Operation: logical.UpdateOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatalf("expected a non-nil response")
|
||||
}
|
||||
|
||||
// List the entities by name
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "entity/name",
|
||||
Operation: logical.ListOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
|
||||
expected := []string{"testentityname2", "testentityname"}
|
||||
sort.Strings(expected)
|
||||
actual := resp.Data["keys"].([]string)
|
||||
sort.Strings(actual)
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("bad: entity list response; expected: %#v\nactual: %#v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityStore_EntityReadGroupIDs(t *testing.T) {
|
||||
var err error
|
||||
var resp *logical.Response
|
||||
|
@ -154,8 +270,8 @@ func TestIdentityStore_EntityCreateUpdate(t *testing.T) {
|
|||
|
||||
func TestIdentityStore_CloneImmutability(t *testing.T) {
|
||||
alias := &identity.Alias{
|
||||
ID: "testaliasid",
|
||||
Name: "testaliasname",
|
||||
ID: "testaliasid",
|
||||
Name: "testaliasname",
|
||||
MergedFromCanonicalIDs: []string{"entityid1"},
|
||||
}
|
||||
|
||||
|
@ -543,41 +659,6 @@ func TestIdentityStore_MemDBEntityIndexes(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
// This test is required because MemDB does not take care of ensuring
|
||||
// uniqueness of indexes that are marked unique. It is the job of the higher
|
||||
// level abstraction, the identity store in this case.
|
||||
func TestIdentityStore_EntitySameEntityNames(t *testing.T) {
|
||||
var err error
|
||||
var resp *logical.Response
|
||||
ctx := namespace.RootContext(nil)
|
||||
is, _, _ := testIdentityStoreWithGithubAuth(ctx, t)
|
||||
|
||||
registerData := map[string]interface{}{
|
||||
"name": "testentityname",
|
||||
}
|
||||
|
||||
registerReq := &logical.Request{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "entity",
|
||||
Data: registerData,
|
||||
}
|
||||
|
||||
// Register an entity
|
||||
resp, err = is.HandleRequest(ctx, registerReq)
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||
}
|
||||
|
||||
// Register another entity with same name
|
||||
resp, err = is.HandleRequest(ctx, registerReq)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp == nil || !resp.IsError() {
|
||||
t.Fatalf("expected an error due to entity name not being unique")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityStore_EntityCRUD(t *testing.T) {
|
||||
var err error
|
||||
var resp *logical.Response
|
||||
|
|
|
@ -19,44 +19,48 @@ const (
|
|||
groupTypeExternal = "external"
|
||||
)
|
||||
|
||||
func groupPaths(i *IdentityStore) []*framework.Path {
|
||||
return []*framework.Path{
|
||||
{
|
||||
Pattern: "group$",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "ID of the group. If set, updates the corresponding existing group.",
|
||||
},
|
||||
"type": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Type of the group, 'internal' or 'external'. Defaults to 'internal'",
|
||||
},
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the group.",
|
||||
},
|
||||
"metadata": {
|
||||
Type: framework.TypeKVPairs,
|
||||
Description: `Metadata to be associated with the group.
|
||||
func groupPathFields() map[string]*framework.FieldSchema {
|
||||
return map[string]*framework.FieldSchema{
|
||||
"id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "ID of the group. If set, updates the corresponding existing group.",
|
||||
},
|
||||
"type": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Type of the group, 'internal' or 'external'. Defaults to 'internal'",
|
||||
},
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the group.",
|
||||
},
|
||||
"metadata": {
|
||||
Type: framework.TypeKVPairs,
|
||||
Description: `Metadata to be associated with the group.
|
||||
In CLI, this parameter can be repeated multiple times, and it all gets merged together.
|
||||
For example:
|
||||
vault <command> <path> metadata=key1=value1 metadata=key2=value2
|
||||
`,
|
||||
},
|
||||
"policies": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Policies to be tied to the group.",
|
||||
},
|
||||
"member_group_ids": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Group IDs to be assigned as group members.",
|
||||
},
|
||||
"member_entity_ids": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Entity IDs to be assigned as group members.",
|
||||
},
|
||||
},
|
||||
},
|
||||
"policies": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Policies to be tied to the group.",
|
||||
},
|
||||
"member_group_ids": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Group IDs to be assigned as group members.",
|
||||
},
|
||||
"member_entity_ids": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Entity IDs to be assigned as group members.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func groupPaths(i *IdentityStore) []*framework.Path {
|
||||
return []*framework.Path{
|
||||
{
|
||||
Pattern: "group$",
|
||||
Fields: groupPathFields(),
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.UpdateOperation: i.pathGroupRegister(),
|
||||
},
|
||||
|
@ -66,41 +70,7 @@ vault <command> <path> metadata=key1=value1 metadata=key2=value2
|
|||
},
|
||||
{
|
||||
Pattern: "group/id/" + framework.GenericNameRegex("id"),
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"id": {
|
||||
Type: framework.TypeString,
|
||||
Description: "ID of the group.",
|
||||
},
|
||||
"type": {
|
||||
Type: framework.TypeString,
|
||||
Default: groupTypeInternal,
|
||||
Description: "Type of the group, 'internal' or 'external'. Defaults to 'internal'",
|
||||
},
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the group.",
|
||||
},
|
||||
"metadata": {
|
||||
Type: framework.TypeKVPairs,
|
||||
Description: `Metadata to be associated with the group.
|
||||
In CLI, this parameter can be repeated multiple times, and it all gets merged together.
|
||||
For example:
|
||||
vault <command> <path> metadata=key1=value1 metadata=key2=value2
|
||||
`,
|
||||
},
|
||||
"policies": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Policies to be tied to the group.",
|
||||
},
|
||||
"member_group_ids": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Group IDs to be assigned as group members.",
|
||||
},
|
||||
"member_entity_ids": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "Entity IDs to be assigned as group members.",
|
||||
},
|
||||
},
|
||||
Fields: groupPathFields(),
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.UpdateOperation: i.pathGroupIDUpdate(),
|
||||
logical.ReadOperation: i.pathGroupIDRead(),
|
||||
|
@ -119,6 +89,27 @@ vault <command> <path> metadata=key1=value1 metadata=key2=value2
|
|||
HelpSynopsis: strings.TrimSpace(groupHelp["group-id-list"][0]),
|
||||
HelpDescription: strings.TrimSpace(groupHelp["group-id-list"][1]),
|
||||
},
|
||||
{
|
||||
Pattern: "group/name/" + framework.GenericNameRegex("name"),
|
||||
Fields: groupPathFields(),
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.UpdateOperation: i.pathGroupNameUpdate(),
|
||||
logical.ReadOperation: i.pathGroupNameRead(),
|
||||
logical.DeleteOperation: i.pathGroupNameDelete(),
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(groupHelp["group-by-name"][0]),
|
||||
HelpDescription: strings.TrimSpace(groupHelp["group-by-name"][1]),
|
||||
},
|
||||
{
|
||||
Pattern: "group/name/?$",
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ListOperation: i.pathGroupNameList(),
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(groupHelp["group-name-list"][0]),
|
||||
HelpDescription: strings.TrimSpace(groupHelp["group-name-list"][1]),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,6 +149,24 @@ func (i *IdentityStore) pathGroupIDUpdate() framework.OperationFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func (i *IdentityStore) pathGroupNameUpdate() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
groupName := d.Get("name").(string)
|
||||
if groupName == "" {
|
||||
return logical.ErrorResponse("empty group name"), nil
|
||||
}
|
||||
|
||||
i.groupLock.Lock()
|
||||
defer i.groupLock.Unlock()
|
||||
|
||||
group, err := i.MemDBGroupByName(ctx, groupName, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.handleGroupUpdateCommon(ctx, req, d, group)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleGroupUpdateCommon(ctx context.Context, req *logical.Request, d *framework.FieldData, group *identity.Group) (*logical.Response, error) {
|
||||
var newGroup bool
|
||||
if group == nil {
|
||||
|
@ -210,8 +219,8 @@ func (i *IdentityStore) handleGroupUpdateCommon(ctx context.Context, req *logica
|
|||
switch {
|
||||
case groupByName == nil:
|
||||
// Allowed
|
||||
case newGroup:
|
||||
return logical.ErrorResponse("updating a group by name is not currently supported"), nil
|
||||
case group.ID == "":
|
||||
group = groupByName
|
||||
case group.ID != "" && groupByName.ID != group.ID:
|
||||
return logical.ErrorResponse("group name is already in use"), nil
|
||||
}
|
||||
|
@ -251,6 +260,10 @@ func (i *IdentityStore) handleGroupUpdateCommon(ctx context.Context, req *logica
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if !newGroup {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
respData := map[string]interface{}{
|
||||
"id": group.ID,
|
||||
"name": group.Name,
|
||||
|
@ -279,6 +292,25 @@ func (i *IdentityStore) pathGroupIDRead() framework.OperationFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func (i *IdentityStore) pathGroupNameRead() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
groupName := d.Get("name").(string)
|
||||
if groupName == "" {
|
||||
return logical.ErrorResponse("empty group name"), nil
|
||||
}
|
||||
|
||||
group, err := i.MemDBGroupByName(ctx, groupName, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if group == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return i.handleGroupReadCommon(ctx, group)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleGroupReadCommon(ctx context.Context, group *identity.Group) (*logical.Response, error) {
|
||||
if group == nil {
|
||||
return nil, nil
|
||||
|
@ -346,124 +378,160 @@ func (i *IdentityStore) pathGroupIDDelete() framework.OperationFunc {
|
|||
return logical.ErrorResponse("empty group ID"), nil
|
||||
}
|
||||
|
||||
if groupID == "" {
|
||||
return nil, fmt.Errorf("missing group ID")
|
||||
return i.handleGroupDeleteCommon(ctx, groupID, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IdentityStore) pathGroupNameDelete() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
groupName := d.Get("name").(string)
|
||||
if groupName == "" {
|
||||
return logical.ErrorResponse("empty group name"), nil
|
||||
}
|
||||
|
||||
// Acquire the lock to modify the group storage entry
|
||||
i.groupLock.Lock()
|
||||
defer i.groupLock.Unlock()
|
||||
return i.handleGroupDeleteCommon(ctx, groupName, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a MemDB transaction to delete group
|
||||
txn := i.db.Txn(true)
|
||||
defer txn.Abort()
|
||||
func (i *IdentityStore) handleGroupDeleteCommon(ctx context.Context, key string, byID bool) (*logical.Response, error) {
|
||||
// Acquire the lock to modify the group storage entry
|
||||
i.groupLock.Lock()
|
||||
defer i.groupLock.Unlock()
|
||||
|
||||
group, err := i.MemDBGroupByIDInTxn(txn, groupID, false)
|
||||
// Create a MemDB transaction to delete group
|
||||
txn := i.db.Txn(true)
|
||||
defer txn.Abort()
|
||||
|
||||
var group *identity.Group
|
||||
var err error
|
||||
switch byID {
|
||||
case true:
|
||||
group, err = i.MemDBGroupByIDInTxn(txn, key, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If there is no group for the ID, do nothing
|
||||
if group == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
default:
|
||||
group, err = i.MemDBGroupByNameInTxn(ctx, txn, key, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if group.NamespaceID != ns.ID {
|
||||
return nil, logical.ErrUnsupportedPath
|
||||
}
|
||||
|
||||
// Delete group alias from memdb
|
||||
if group.Type == groupTypeExternal && group.Alias != nil {
|
||||
err = i.MemDBDeleteAliasByIDInTxn(txn, group.Alias.ID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the group using the same transaction
|
||||
err = i.MemDBDeleteGroupByIDInTxn(txn, group.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Delete the group from storage
|
||||
err = i.groupPacker.DeleteItem(group.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Committing the transaction *after* successfully deleting group
|
||||
txn.Commit()
|
||||
|
||||
}
|
||||
if group == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if group.NamespaceID != ns.ID {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Delete group alias from memdb
|
||||
if group.Type == groupTypeExternal && group.Alias != nil {
|
||||
err = i.MemDBDeleteAliasByIDInTxn(txn, group.Alias.ID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the group using the same transaction
|
||||
err = i.MemDBDeleteGroupByIDInTxn(txn, group.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Delete the group from storage
|
||||
err = i.groupPacker.DeleteItem(group.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Committing the transaction *after* successfully deleting group
|
||||
txn.Commit()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// pathGroupIDList lists the IDs of all the groups in the identity store
|
||||
func (i *IdentityStore) pathGroupIDList() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return i.handleGroupListCommon(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
// pathGroupNameList lists the names of all the groups in the identity store
|
||||
func (i *IdentityStore) pathGroupNameList() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
return i.handleGroupListCommon(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IdentityStore) handleGroupListCommon(ctx context.Context, byID bool) (*logical.Response, error) {
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txn := i.db.Txn(false)
|
||||
|
||||
iter, err := txn.Get(groupsTable, "namespace_id", ns.ID)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed to lookup groups using namespace ID: {{err}}", err)
|
||||
}
|
||||
|
||||
var keys []string
|
||||
groupInfo := map[string]interface{}{}
|
||||
|
||||
type mountInfo struct {
|
||||
MountType string
|
||||
MountPath string
|
||||
}
|
||||
mountAccessorMap := map[string]mountInfo{}
|
||||
|
||||
for entry := iter.Next(); entry != nil; entry = iter.Next() {
|
||||
group := entry.(*identity.Group)
|
||||
|
||||
if byID {
|
||||
keys = append(keys, group.ID)
|
||||
} else {
|
||||
keys = append(keys, group.Name)
|
||||
}
|
||||
|
||||
txn := i.db.Txn(false)
|
||||
|
||||
iter, err := txn.Get(groupsTable, "namespace_id", ns.ID)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed to lookup groups using namespace ID: {{err}}", err)
|
||||
groupInfoEntry := map[string]interface{}{
|
||||
"name": group.Name,
|
||||
"num_member_entities": len(group.MemberEntityIDs),
|
||||
"num_parent_groups": len(group.ParentGroupIDs),
|
||||
}
|
||||
|
||||
var groupIDs []string
|
||||
groupInfo := map[string]interface{}{}
|
||||
|
||||
type mountInfo struct {
|
||||
MountType string
|
||||
MountPath string
|
||||
}
|
||||
mountAccessorMap := map[string]mountInfo{}
|
||||
|
||||
for entry := iter.Next(); entry != nil; entry = iter.Next() {
|
||||
group := entry.(*identity.Group)
|
||||
groupIDs = append(groupIDs, group.ID)
|
||||
groupInfoEntry := map[string]interface{}{
|
||||
"name": group.Name,
|
||||
"num_member_entities": len(group.MemberEntityIDs),
|
||||
"num_parent_groups": len(group.ParentGroupIDs),
|
||||
if group.Alias != nil {
|
||||
entry := map[string]interface{}{
|
||||
"id": group.Alias.ID,
|
||||
"name": group.Alias.Name,
|
||||
"mount_accessor": group.Alias.MountAccessor,
|
||||
}
|
||||
if group.Alias != nil {
|
||||
entry := map[string]interface{}{
|
||||
"id": group.Alias.ID,
|
||||
"name": group.Alias.Name,
|
||||
"mount_accessor": group.Alias.MountAccessor,
|
||||
}
|
||||
|
||||
mi, ok := mountAccessorMap[group.Alias.MountAccessor]
|
||||
if ok {
|
||||
mi, ok := mountAccessorMap[group.Alias.MountAccessor]
|
||||
if ok {
|
||||
entry["mount_type"] = mi.MountType
|
||||
entry["mount_path"] = mi.MountPath
|
||||
} else {
|
||||
mi = mountInfo{}
|
||||
if mountValidationResp := i.core.router.validateMountByAccessor(group.Alias.MountAccessor); mountValidationResp != nil {
|
||||
mi.MountType = mountValidationResp.MountType
|
||||
mi.MountPath = mountValidationResp.MountPath
|
||||
entry["mount_type"] = mi.MountType
|
||||
entry["mount_path"] = mi.MountPath
|
||||
} else {
|
||||
mi = mountInfo{}
|
||||
if mountValidationResp := i.core.router.validateMountByAccessor(group.Alias.MountAccessor); mountValidationResp != nil {
|
||||
mi.MountType = mountValidationResp.MountType
|
||||
mi.MountPath = mountValidationResp.MountPath
|
||||
entry["mount_type"] = mi.MountType
|
||||
entry["mount_path"] = mi.MountPath
|
||||
}
|
||||
mountAccessorMap[group.Alias.MountAccessor] = mi
|
||||
}
|
||||
|
||||
groupInfoEntry["alias"] = entry
|
||||
mountAccessorMap[group.Alias.MountAccessor] = mi
|
||||
}
|
||||
groupInfo[group.ID] = groupInfoEntry
|
||||
}
|
||||
|
||||
return logical.ListResponseWithInfo(groupIDs, groupInfo), nil
|
||||
groupInfoEntry["alias"] = entry
|
||||
}
|
||||
groupInfo[group.ID] = groupInfoEntry
|
||||
}
|
||||
|
||||
return logical.ListResponseWithInfo(keys, groupInfo), nil
|
||||
}
|
||||
|
||||
var groupHelp = map[string][2]string{
|
||||
|
|
|
@ -11,6 +11,122 @@ import (
|
|||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
||||
func TestIdentityStore_GroupByName(t *testing.T) {
|
||||
ctx := namespace.RootContext(nil)
|
||||
i, _, _ := testIdentityStoreWithGithubAuth(ctx, t)
|
||||
|
||||
// Create an entity using the "name" endpoint
|
||||
resp, err := i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "group/name/testgroupname",
|
||||
Operation: logical.UpdateOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatalf("expected a non-nil response")
|
||||
}
|
||||
|
||||
// Test the read by name endpoint
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "group/name/testgroupname",
|
||||
Operation: logical.ReadOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp == nil || resp.Data["name"].(string) != "testgroupname" {
|
||||
t.Fatalf("bad entity response: %#v", resp)
|
||||
}
|
||||
|
||||
// Update group metadata using the name endpoint
|
||||
groupMetadata := map[string]string{
|
||||
"foo": "bar",
|
||||
}
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "group/name/testgroupname",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"metadata": groupMetadata,
|
||||
},
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
|
||||
// Check the updated result
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "group/name/testgroupname",
|
||||
Operation: logical.ReadOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp == nil || !reflect.DeepEqual(resp.Data["metadata"].(map[string]string), groupMetadata) {
|
||||
t.Fatalf("bad group response: %#v", resp)
|
||||
}
|
||||
|
||||
// Delete the group using the name endpoint
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "group/name/testgroupname",
|
||||
Operation: logical.DeleteOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
|
||||
// Check if deletion was successful
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "group/name/testgroupname",
|
||||
Operation: logical.ReadOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp != nil {
|
||||
t.Fatalf("expected a nil response")
|
||||
}
|
||||
|
||||
// Create 2 entities
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "group/name/testgroupname",
|
||||
Operation: logical.UpdateOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatalf("expected a non-nil response")
|
||||
}
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "group/name/testgroupname2",
|
||||
Operation: logical.UpdateOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatalf("expected a non-nil response")
|
||||
}
|
||||
|
||||
// List the entities by name
|
||||
resp, err = i.HandleRequest(ctx, &logical.Request{
|
||||
Path: "group/name",
|
||||
Operation: logical.ListOperation,
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
|
||||
}
|
||||
|
||||
expected := []string{"testgroupname2", "testgroupname"}
|
||||
sort.Strings(expected)
|
||||
actual := resp.Data["keys"].([]string)
|
||||
sort.Strings(actual)
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("bad: group list response; expected: %#v\nactual: %#v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityStore_Groups_TypeMembershipAdditions(t *testing.T) {
|
||||
var err error
|
||||
var resp *logical.Response
|
||||
|
|
|
@ -581,13 +581,21 @@ func (i *IdentityStore) MemDBEntityByName(ctx context.Context, entityName string
|
|||
return nil, fmt.Errorf("missing entity name")
|
||||
}
|
||||
|
||||
txn := i.db.Txn(false)
|
||||
|
||||
return i.MemDBEntityByNameInTxn(txn, ctx, entityName, clone)
|
||||
}
|
||||
|
||||
func (i *IdentityStore) MemDBEntityByNameInTxn(txn *memdb.Txn, ctx context.Context, entityName string, clone bool) (*identity.Entity, error) {
|
||||
if entityName == "" {
|
||||
return nil, fmt.Errorf("missing entity name")
|
||||
}
|
||||
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txn := i.db.Txn(false)
|
||||
|
||||
entityRaw, err := txn.First(entitiesTable, "name", ns.ID, entityName)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed to fetch entity from memdb using entity name: {{err}}", err)
|
||||
|
|
|
@ -218,6 +218,158 @@ $ curl \
|
|||
}
|
||||
```
|
||||
|
||||
## Create/Update Entity by Name
|
||||
|
||||
This endpoint is used to create or update an entity by a given name.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :------- | :------------------------------- | :--------------------- |
|
||||
| `POST` | `/identity/entity/name/:name` | `200 application/json` |
|
||||
|
||||
### Parameters
|
||||
|
||||
- `name` `(string: entity-<UUID>)` – Name of the entity.
|
||||
|
||||
- `metadata` `(key-value-map: {})` – Metadata to be associated with the entity.
|
||||
|
||||
- `policies` `(list of strings: [])` – Policies to be tied to the entity.
|
||||
|
||||
- `disabled` `(bool: false)` – Whether the entity is disabled. Disabled
|
||||
entities' associated tokens cannot be used, but are not revoked.
|
||||
|
||||
### Sample Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"organization": "hashi",
|
||||
"team": "nomad"
|
||||
},
|
||||
"policies": ["eng-developers", "infra-developers"]
|
||||
}
|
||||
```
|
||||
|
||||
### Sample Request
|
||||
|
||||
```
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
http://127.0.0.1:8200/v1/identity/entity/name/testentityname
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"aliases": null,
|
||||
"id": "0826be06-577c-a076-3942-2f92da0310ce"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Read Entity by Name
|
||||
|
||||
This endpoint queries the entity by its name.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :------- | :------------------------------- | :--------------------- |
|
||||
| `GET` | `/identity/entity/name/:name` | `200 application/json` |
|
||||
|
||||
### Parameters
|
||||
|
||||
- `name` `(string: <required>)` – Name of the entity.
|
||||
|
||||
### Sample Request
|
||||
|
||||
```
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/identity/entity/id/testentityname
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"aliases": [],
|
||||
"creation_time": "2018-09-19T17:20:27.705389973Z",
|
||||
"direct_group_ids": [],
|
||||
"disabled": false,
|
||||
"group_ids": [],
|
||||
"id": "0826be06-577c-a076-3942-2f92da0310ce",
|
||||
"inherited_group_ids": [],
|
||||
"last_update_time": "2018-09-19T17:20:27.705389973Z",
|
||||
"merged_entity_ids": null,
|
||||
"metadata": {
|
||||
"organization": "hashi",
|
||||
"team": "nomad"
|
||||
},
|
||||
"name": "testentityname",
|
||||
"policies": [
|
||||
"eng-developers",
|
||||
"infra-developers"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Delete Entity by Name
|
||||
|
||||
This endpoint deletes an entity and all its associated aliases, given the
|
||||
entity name.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :--------- | :------------------------------ | :----------------------|
|
||||
| `DELETE` | `/identity/entity/name/:name` | `204 (empty body)` |
|
||||
|
||||
## Parameters
|
||||
|
||||
- `name` `(string: <required>)` – Name of the entity.
|
||||
|
||||
### Sample Request
|
||||
|
||||
```
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
--request DELETE \
|
||||
http://127.0.0.1:8200/v1/identity/entity/name/testentityname
|
||||
```
|
||||
|
||||
## List Entities by Name
|
||||
|
||||
This endpoint returns a list of available entities by their names.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :------- | :-------------------------------- | :--------------------- |
|
||||
| `LIST` | `/identity/entity/name` | `200 application/json` |
|
||||
| `GET` | `/identity/entity/name?list=true` | `200 application/json` |
|
||||
|
||||
|
||||
### Sample Request
|
||||
|
||||
```
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
--request LIST \
|
||||
http://127.0.0.1:8200/v1/identity/entity/name
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"keys": [
|
||||
"testentityname",
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Merge Entities
|
||||
|
||||
This endpoint merges many entities into one entity.
|
||||
|
|
|
@ -228,3 +228,163 @@ $ curl \
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Create/Update Group by Name
|
||||
|
||||
This endpoint is used to create or update a group by its name.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :------- | :------------------------------ | :--------------------- |
|
||||
| `POST` | `/identity/group/name/:name` | `200 application/json` |
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
- `name` `(string: entity-<UUID>)` – Name of the group.
|
||||
|
||||
- `type` `(string: "internal")` - Type of the group, `internal` or `external`.
|
||||
Defaults to `internal`.
|
||||
|
||||
- `metadata` `(key-value-map: {})` – Metadata to be associated with the
|
||||
group.
|
||||
|
||||
- `policies` `(list of strings: [])` – Policies to be tied to the group.
|
||||
|
||||
- `member_group_ids` `(list of strings: [])` - Group IDs to be assigned as
|
||||
group members.
|
||||
|
||||
- `member_entity_ids` `(list of strings: [])` - Entity IDs to be assigned as
|
||||
group members.
|
||||
|
||||
### Sample Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"hello": "everyone"
|
||||
},
|
||||
"policies": ["grouppolicy2", "grouppolicy3"]
|
||||
}
|
||||
```
|
||||
|
||||
### Sample Request
|
||||
|
||||
```
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
http://127.0.0.1:8200/v1/identity/group/name/testgroupname
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
```json
|
||||
{
|
||||
"request_id": "b98b4a3d-a9f1-e151-11e1-ad91cfb08351",
|
||||
"lease_id": "",
|
||||
"lease_duration": 0,
|
||||
"renewable": false,
|
||||
"data": {
|
||||
"id": "5a3a04a0-0c3a-a4c3-74e8-26b1adbeaece",
|
||||
"name": "testgroupname"
|
||||
},
|
||||
"warnings": null
|
||||
}
|
||||
```
|
||||
|
||||
## Read Group by Name
|
||||
|
||||
This endpoint queries the group by its name.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :------- | :------------------------------ | :--------------------- |
|
||||
| `GET` | `/identity/group/name/:name` | `200 application/json` |
|
||||
|
||||
### Parameters
|
||||
|
||||
- `name` `(string: <required>)` – Name of the group.
|
||||
|
||||
### Sample Request
|
||||
|
||||
```
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/identity/group/name/testgroupname
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"alias": {},
|
||||
"creation_time": "2018-09-19T22:02:04.395128091Z",
|
||||
"id": "5a3a04a0-0c3a-a4c3-74e8-26b1adbeaece",
|
||||
"last_update_time": "2018-09-19T22:02:04.395128091Z",
|
||||
"member_entity_ids": [],
|
||||
"member_group_ids": null,
|
||||
"metadata": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"modify_index": 1,
|
||||
"name": "testgroupname",
|
||||
"parent_group_ids": null,
|
||||
"policies": [
|
||||
"grouppolicy1",
|
||||
"grouppolicy2"
|
||||
],
|
||||
"type": "internal"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Delete Group by Name
|
||||
|
||||
This endpoint deletes a group, given its name.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :--------- | :----------------------------- | :----------------------|
|
||||
| `DELETE` | `/identity/group/name/:name` | `204 (empty body)` |
|
||||
|
||||
## Parameters
|
||||
|
||||
- `name` `(string: <required>)` – Name of the group.
|
||||
|
||||
### Sample Request
|
||||
|
||||
```
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
--request DELETE \
|
||||
http://127.0.0.1:8200/v1/identity/group/name/testgroupname
|
||||
```
|
||||
|
||||
## List Groups by Name
|
||||
|
||||
This endpoint returns a list of available groups by their names.
|
||||
|
||||
| Method | Path | Produces |
|
||||
| :------- | :------------------------------- | :--------------------- |
|
||||
| `LIST` | `/identity/group/name` | `200 application/json` |
|
||||
| `GET` | `/identity/group/name?list=true` | `200 application/json` |
|
||||
|
||||
### Sample Request
|
||||
|
||||
```
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
--request LIST \
|
||||
http://127.0.0.1:8200/v1/identity/group/name
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"keys": [
|
||||
"testgroupname"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue