diff --git a/helper/identity/identity.go b/helper/identity/identity.go index 8c04401536..9f9f84f2ad 100644 --- a/helper/identity/identity.go +++ b/helper/identity/identity.go @@ -192,3 +192,15 @@ func ToSDKGroups(groups []*Group) []*logical.Group { } return ret } + +func (g *Group) SCIMClientID() string { + return g.ScimClientID +} + +func (e *Entity) SCIMClientID() string { + return e.ScimClientID +} + +func (a *Alias) SCIMClientID() string { + return a.ScimClientID +} diff --git a/vault/identity_store_aliases.go b/vault/identity_store_aliases.go index 4bce45ac50..7697b441aa 100644 --- a/vault/identity_store_aliases.go +++ b/vault/identity_store_aliases.go @@ -279,6 +279,9 @@ func (i *IdentityStore) handleAliasCreate(ctx context.Context, canonicalID, name return nil, err } + if err := i.scimResourceCheck(ctx, &identity.Alias{ScimClientID: scimClientID}, "", true); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrPermissionDenied + } var entity *identity.Entity if canonicalID != "" { entity, err = i.MemDBEntityByID(canonicalID, true) @@ -370,6 +373,9 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, canonicalID, name return nil, nil } + if err := i.scimResourceCheck(ctx, alias, alias.ScimClientID, false); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrPermissionDenied + } alias.LastUpdateTime = timestamppb.Now() // Get our current entity, which may be the same as the new one if the @@ -604,6 +610,11 @@ func (i *IdentityStore) pathAliasIDDelete() framework.OperationFunc { return logical.ErrorResponse("request and alias are in different namespaces"), logical.ErrPermissionDenied } + scimClientID := scimClientIDFromContext(ctx) + if alias.ScimClientID != scimClientID { + return logical.ErrorResponse("SCIM-managed resources must be modified through SCIM"), logical.ErrPermissionDenied + } + // Fetch the associated entity entity, err := i.MemDBEntityByAliasIDInTxn(txn, alias.ID, true) if err != nil { diff --git a/vault/identity_store_entities.go b/vault/identity_store_entities.go index b95e909c7f..50e2884be7 100644 --- a/vault/identity_store_entities.go +++ b/vault/identity_store_entities.go @@ -597,7 +597,10 @@ func (i *IdentityStore) handleEntityDeleteCommon(ctx context.Context, txn *memdb if entity.NamespaceID != ns.ID { return nil } - + scimClientID := scimClientIDFromContext(ctx) + if entity.ScimClientID != scimClientID { + return errors.New("SCIM-managed resources must be modified through SCIM") + } // Remove entity ID as a member from all the groups it belongs, both // internal and external groups, err := i.MemDBGroupsByMemberEntityIDInTxn(txn, entity.ID, true, false) diff --git a/vault/identity_store_entities_update.go b/vault/identity_store_entities_update.go index a74d04274f..bee8cbdcc8 100644 --- a/vault/identity_store_entities_update.go +++ b/vault/identity_store_entities_update.go @@ -15,10 +15,11 @@ import ( // EntityBuilder is used to construct or update an identity.Entity. type EntityBuilder struct { - store *IdentityStore - entity *identity.Entity - isNew bool - err error + store *IdentityStore + entity *identity.Entity + isNew bool + originalSCIMID string + err error } // NewEntityBuilder creates a new builder instance. @@ -57,6 +58,7 @@ func (b *EntityBuilder) WithID(id string) *EntityBuilder { } b.entity = entity + b.originalSCIMID = b.entity.ScimClientID b.isNew = false return b } @@ -76,6 +78,7 @@ func (b *EntityBuilder) WithExternalID(ctx context.Context, externalID string) * if entityByExternalID != nil { // An entity with this external ID already exists, so we'll update it. b.entity = entityByExternalID + b.originalSCIMID = b.entity.ScimClientID b.isNew = false } else { // No entity found, so we're just setting the external ID on the current one. @@ -103,6 +106,7 @@ func (b *EntityBuilder) WithName(ctx context.Context, name string) *EntityBuilde case b.isNew: // We haven't loaded an entity yet, but one with this name exists. Let's update it. b.entity = entityByName + b.originalSCIMID = b.entity.ScimClientID b.isNew = false case b.entity.ID == entityByName.ID: // The loaded entity and the one found by name are the same. No-op. @@ -167,6 +171,10 @@ func (b *EntityBuilder) Build(ctx context.Context) (*logical.Response, error) { return logical.ErrorResponse(b.err.Error()), nil } + if err := b.store.scimResourceCheck(ctx, b.entity, b.originalSCIMID, b.isNew); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrPermissionDenied + } + // Sanitize and persist the entity if err := b.store.sanitizeEntity(ctx, b.entity); err != nil { return nil, err diff --git a/vault/identity_store_groups.go b/vault/identity_store_groups.go index c475c566c6..c51da8a5e2 100644 --- a/vault/identity_store_groups.go +++ b/vault/identity_store_groups.go @@ -313,12 +313,15 @@ func (i *IdentityStore) handleGroupUpdateCommon(ctx context.Context, req *logica group.Name = groupName } - _, ok = d.Schema["scim_client_id"] + originalSCIMID := group.ScimClientID + scimClientID, ok := d.GetOk("scim_client_id") if ok { - entitSCIMClientID := d.Get("scim_client_id").(string) - group.ScimClientID = entitSCIMClientID + group.ScimClientID = scimClientID.(string) } + if err := i.scimResourceCheck(ctx, group, originalSCIMID, newGroup); err != nil { + return logical.ErrorResponse(err.Error()), nil + } metadata, ok, err := d.GetOkErr("metadata") if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to parse metadata: %v", err)), nil @@ -525,6 +528,11 @@ func (i *IdentityStore) handleGroupDeleteCommon(ctx context.Context, key string, return logical.ErrorResponse("request namespace is not the same as the group namespace"), logical.ErrPermissionDenied } + scimID := scimClientIDFromContext(ctx) + if scimID != group.ScimClientID { + return logical.ErrorResponse("SCIM-managed resources must be modified through SCIM"), logical.ErrPermissionDenied + } + // Delete group alias from memdb if group.Type == groupTypeExternal && group.Alias != nil { err = i.MemDBDeleteAliasByIDInTxn(txn, group.Alias.ID, true) diff --git a/vault/identity_store_scim_schema.go b/vault/identity_store_scim_schema.go index 9a9393f66a..8c672dc7a4 100644 --- a/vault/identity_store_scim_schema.go +++ b/vault/identity_store_scim_schema.go @@ -3,7 +3,13 @@ package vault -import "github.com/hashicorp/go-memdb" +import ( + "context" + "errors" + + "github.com/hashicorp/go-memdb" + "github.com/hashicorp/vault/helper/identity" +) // SCIM client storage prefix const scimClientStoragePrefix = "scim/client/" @@ -69,3 +75,61 @@ func scimClientSchema(_ bool) *memdb.TableSchema { }, } } + +type scimClientRequest struct{} + +func addSCIMClientIDToContext(ctx context.Context, id string) context.Context { + return context.WithValue(ctx, scimClientRequest{}, id) +} + +func scimClientIDFromContext(ctx context.Context) string { + val := ctx.Value(scimClientRequest{}) + if val == nil { + return "" + } + return val.(string) +} + +func (i *IdentityStore) scimResourceCheck(ctx context.Context, resource scimManaged, originalSCIMID string, isCreate bool) error { + reqSCIMClientID := scimClientIDFromContext(ctx) + resourceSCIMClientID := resource.SCIMClientID() + + switch isCreate { + case true: + // The request must have come via a SCIM API in order to set + // the SCIM client ID + if reqSCIMClientID == "" && resourceSCIMClientID != "" { + return errors.New("cannot set scim_client_id") + } + if reqSCIMClientID != "" && resourceSCIMClientID == "" { + // this shouldn't ever happen + return errors.New("cannot create a resource via SCIM without a SCIM client ID") + } + if reqSCIMClientID != resourceSCIMClientID { + // this also shouldn't ever happen + return errors.New("cannot create resource via SCIM with a different SCIM client ID") + } + + default: + // if the resource is being updated, the SCIM client ID + // cannot be modified + if originalSCIMID != resourceSCIMClientID { + return errors.New("cannot update scim_client_id") + } + // if the resource is being updated, this must be via SCIM + if originalSCIMID != reqSCIMClientID { + return errors.New("SCIM-managed resources must be modified through SCIM") + } + } + return nil +} + +type scimManaged interface { + SCIMClientID() string +} + +var ( + _ scimManaged = (*identity.Entity)(nil) + _ scimManaged = (*identity.Group)(nil) + _ scimManaged = (*identity.Alias)(nil) +)