622 lines
17 KiB
Go
622 lines
17 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package vault
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/golang/protobuf/ptypes"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
"github.com/hashicorp/vault/helper/identity"
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
const (
|
|
groupTypeInternal = "internal"
|
|
groupTypeExternal = "external"
|
|
)
|
|
|
|
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.",
|
|
},
|
|
}
|
|
}
|
|
|
|
func groupPaths(i *IdentityStore) []*framework.Path {
|
|
return []*framework.Path{
|
|
{
|
|
Pattern: "group$",
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: "group",
|
|
OperationVerb: "create",
|
|
},
|
|
|
|
Fields: groupPathFields(),
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: i.pathGroupRegister(),
|
|
ForwardPerformanceStandby: true,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: strings.TrimSpace(groupHelp["register"][0]),
|
|
HelpDescription: strings.TrimSpace(groupHelp["register"][1]),
|
|
},
|
|
{
|
|
Pattern: "group/id/" + framework.GenericNameRegex("id"),
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: "group",
|
|
OperationSuffix: "by-id",
|
|
},
|
|
|
|
Fields: groupPathFields(),
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: i.pathGroupIDUpdate(),
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationVerb: "update",
|
|
},
|
|
},
|
|
logical.ReadOperation: &framework.PathOperation{
|
|
Callback: i.pathGroupIDRead(),
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationVerb: "read",
|
|
},
|
|
},
|
|
logical.DeleteOperation: &framework.PathOperation{
|
|
Callback: i.pathGroupIDDelete(),
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationVerb: "delete",
|
|
},
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: strings.TrimSpace(groupHelp["group-by-id"][0]),
|
|
HelpDescription: strings.TrimSpace(groupHelp["group-by-id"][1]),
|
|
},
|
|
{
|
|
Pattern: "group/id/?$",
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: "group",
|
|
OperationSuffix: "by-id",
|
|
},
|
|
|
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
|
logical.ListOperation: i.pathGroupIDList(),
|
|
},
|
|
|
|
HelpSynopsis: strings.TrimSpace(groupHelp["group-id-list"][0]),
|
|
HelpDescription: strings.TrimSpace(groupHelp["group-id-list"][1]),
|
|
},
|
|
{
|
|
Pattern: "group/name/(?P<name>.+)",
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: "group",
|
|
OperationSuffix: "by-name",
|
|
},
|
|
|
|
Fields: groupPathFields(),
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: i.pathGroupNameUpdate(),
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationVerb: "update",
|
|
},
|
|
},
|
|
logical.ReadOperation: &framework.PathOperation{
|
|
Callback: i.pathGroupNameRead(),
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationVerb: "read",
|
|
},
|
|
},
|
|
logical.DeleteOperation: &framework.PathOperation{
|
|
Callback: i.pathGroupNameDelete(),
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationVerb: "delete",
|
|
},
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: strings.TrimSpace(groupHelp["group-by-name"][0]),
|
|
HelpDescription: strings.TrimSpace(groupHelp["group-by-name"][1]),
|
|
},
|
|
{
|
|
Pattern: "group/name/?$",
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: "group",
|
|
OperationSuffix: "by-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]),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (i *IdentityStore) pathGroupRegister() framework.OperationFunc {
|
|
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
_, ok := d.GetOk("id")
|
|
if ok {
|
|
return i.pathGroupIDUpdate()(ctx, req, d)
|
|
}
|
|
|
|
_, ok = d.GetOk("name")
|
|
if ok {
|
|
return i.pathGroupNameUpdate()(ctx, req, d)
|
|
}
|
|
|
|
i.groupLock.Lock()
|
|
defer i.groupLock.Unlock()
|
|
|
|
return i.handleGroupUpdateCommon(ctx, req, d, nil)
|
|
}
|
|
}
|
|
|
|
func (i *IdentityStore) pathGroupIDUpdate() framework.OperationFunc {
|
|
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
groupID := d.Get("id").(string)
|
|
if groupID == "" {
|
|
return logical.ErrorResponse("empty group ID"), nil
|
|
}
|
|
|
|
i.groupLock.Lock()
|
|
defer i.groupLock.Unlock()
|
|
|
|
group, err := i.MemDBGroupByID(groupID, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if group == nil {
|
|
return logical.ErrorResponse("invalid group ID"), nil
|
|
}
|
|
|
|
return i.handleGroupUpdateCommon(ctx, req, d, group)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
group = new(identity.Group)
|
|
newGroup = true
|
|
}
|
|
|
|
// Update the policies if supplied
|
|
policiesRaw, ok := d.GetOk("policies")
|
|
if ok {
|
|
group.Policies = strutil.RemoveDuplicatesStable(policiesRaw.([]string), true)
|
|
}
|
|
|
|
if strutil.StrListContains(group.Policies, "root") {
|
|
return logical.ErrorResponse("policies cannot contain root"), nil
|
|
}
|
|
|
|
groupTypeRaw, ok := d.GetOk("type")
|
|
if ok {
|
|
groupType := groupTypeRaw.(string)
|
|
if group.Type != "" && groupType != group.Type {
|
|
return logical.ErrorResponse(fmt.Sprintf("group type cannot be changed")), nil
|
|
}
|
|
|
|
group.Type = groupType
|
|
}
|
|
|
|
// If group type is not set, default to internal type
|
|
if group.Type == "" {
|
|
group.Type = groupTypeInternal
|
|
}
|
|
|
|
if group.Type != groupTypeInternal && group.Type != groupTypeExternal {
|
|
return logical.ErrorResponse(fmt.Sprintf("invalid group type %q", group.Type)), nil
|
|
}
|
|
|
|
// Get the name
|
|
groupName := d.Get("name").(string)
|
|
if groupName != "" {
|
|
// Check if there is a group already existing for the given name
|
|
groupByName, err := i.MemDBGroupByName(ctx, groupName, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If no existing group has this name, go ahead with the creation or rename.
|
|
// If there is a group, it must match the group passed in; groupByName
|
|
// should not be modified as it's in memdb.
|
|
switch {
|
|
case groupByName == nil:
|
|
// Allowed
|
|
case groupByName.ID != group.ID:
|
|
return logical.ErrorResponse("group name is already in use"), nil
|
|
}
|
|
group.Name = groupName
|
|
}
|
|
|
|
metadata, ok, err := d.GetOkErr("metadata")
|
|
if err != nil {
|
|
return logical.ErrorResponse(fmt.Sprintf("failed to parse metadata: %v", err)), nil
|
|
}
|
|
if ok {
|
|
group.Metadata = metadata.(map[string]string)
|
|
}
|
|
|
|
memberEntityIDsRaw, ok := d.GetOk("member_entity_ids")
|
|
if ok {
|
|
if group.Type == groupTypeExternal {
|
|
return logical.ErrorResponse("member entities can't be set manually for external groups"), nil
|
|
}
|
|
group.MemberEntityIDs = memberEntityIDsRaw.([]string)
|
|
}
|
|
|
|
memberGroupIDsRaw, ok := d.GetOk("member_group_ids")
|
|
var memberGroupIDs []string
|
|
if ok {
|
|
if group.Type == groupTypeExternal {
|
|
return logical.ErrorResponse("member groups can't be set for external groups"), nil
|
|
}
|
|
memberGroupIDs = memberGroupIDsRaw.([]string)
|
|
}
|
|
|
|
err = i.sanitizeAndUpsertGroup(ctx, group, nil, memberGroupIDs)
|
|
if err != nil {
|
|
if errStr := err.Error(); strings.HasPrefix(errStr, errCycleDetectedPrefix) {
|
|
return logical.ErrorResponse(errStr), nil
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
if !newGroup {
|
|
return nil, nil
|
|
}
|
|
|
|
respData := map[string]interface{}{
|
|
"id": group.ID,
|
|
"name": group.Name,
|
|
}
|
|
return &logical.Response{
|
|
Data: respData,
|
|
}, nil
|
|
}
|
|
|
|
func (i *IdentityStore) pathGroupIDRead() framework.OperationFunc {
|
|
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
groupID := d.Get("id").(string)
|
|
if groupID == "" {
|
|
return logical.ErrorResponse("empty group id"), nil
|
|
}
|
|
|
|
group, err := i.MemDBGroupByID(groupID, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if group == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return i.handleGroupReadCommon(ctx, group)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
ns, err := namespace.FromContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ns.ID != group.NamespaceID {
|
|
return logical.ErrorResponse("request namespace is not the same as the group namespace"), logical.ErrPermissionDenied
|
|
}
|
|
|
|
respData := map[string]interface{}{}
|
|
respData["id"] = group.ID
|
|
respData["name"] = group.Name
|
|
respData["policies"] = group.Policies
|
|
respData["member_entity_ids"] = group.MemberEntityIDs
|
|
respData["parent_group_ids"] = group.ParentGroupIDs
|
|
respData["metadata"] = group.Metadata
|
|
respData["creation_time"] = ptypes.TimestampString(group.CreationTime)
|
|
respData["last_update_time"] = ptypes.TimestampString(group.LastUpdateTime)
|
|
respData["modify_index"] = group.ModifyIndex
|
|
respData["type"] = group.Type
|
|
respData["namespace_id"] = group.NamespaceID
|
|
|
|
aliasMap := map[string]interface{}{}
|
|
if group.Alias != nil {
|
|
aliasMap["id"] = group.Alias.ID
|
|
aliasMap["canonical_id"] = group.Alias.CanonicalID
|
|
aliasMap["mount_accessor"] = group.Alias.MountAccessor
|
|
aliasMap["metadata"] = group.Alias.Metadata
|
|
aliasMap["name"] = group.Alias.Name
|
|
aliasMap["merged_from_canonical_ids"] = group.Alias.MergedFromCanonicalIDs
|
|
aliasMap["creation_time"] = ptypes.TimestampString(group.Alias.CreationTime)
|
|
aliasMap["last_update_time"] = ptypes.TimestampString(group.Alias.LastUpdateTime)
|
|
|
|
if mountValidationResp := i.router.ValidateMountByAccessor(group.Alias.MountAccessor); mountValidationResp != nil {
|
|
aliasMap["mount_path"] = mountValidationResp.MountPath
|
|
aliasMap["mount_type"] = mountValidationResp.MountType
|
|
}
|
|
}
|
|
|
|
respData["alias"] = aliasMap
|
|
|
|
var memberGroupIDs []string
|
|
memberGroups, err := i.MemDBGroupsByParentGroupID(group.ID, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, memberGroup := range memberGroups {
|
|
memberGroupIDs = append(memberGroupIDs, memberGroup.ID)
|
|
}
|
|
|
|
respData["member_group_ids"] = memberGroupIDs
|
|
|
|
return &logical.Response{
|
|
Data: respData,
|
|
}, nil
|
|
}
|
|
|
|
func (i *IdentityStore) pathGroupIDDelete() framework.OperationFunc {
|
|
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
groupID := d.Get("id").(string)
|
|
if groupID == "" {
|
|
return logical.ErrorResponse("empty group ID"), nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return i.handleGroupDeleteCommon(ctx, groupName, false)
|
|
}
|
|
}
|
|
|
|
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()
|
|
|
|
// 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
|
|
}
|
|
default:
|
|
group, err = i.MemDBGroupByNameInTxn(ctx, txn, key, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if group == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
ns, err := namespace.FromContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if group.NamespaceID != ns.ID {
|
|
return logical.ErrorResponse("request namespace is not the same as the group namespace"), logical.ErrPermissionDenied
|
|
}
|
|
|
|
// 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(ctx, 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) {
|
|
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, fmt.Errorf("failed to lookup groups using namespace ID: %w", 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)
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
mi, ok := mountAccessorMap[group.Alias.MountAccessor]
|
|
if ok {
|
|
entry["mount_type"] = mi.MountType
|
|
entry["mount_path"] = mi.MountPath
|
|
} else {
|
|
mi = mountInfo{}
|
|
if mountValidationResp := i.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
|
|
}
|
|
groupInfo[group.ID] = groupInfoEntry
|
|
}
|
|
|
|
return logical.ListResponseWithInfo(keys, groupInfo), nil
|
|
}
|
|
|
|
var groupHelp = map[string][2]string{
|
|
"register": {
|
|
"Create a new group.",
|
|
"",
|
|
},
|
|
"group-by-id": {
|
|
"Update or delete an existing group using its ID.",
|
|
"",
|
|
},
|
|
"group-id-list": {
|
|
"List all the group IDs.",
|
|
"",
|
|
},
|
|
}
|