open-vault/vault/identity_store_groups.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.",
"",
},
}