diff --git a/api/v1alpha1/user_types.go b/api/v1alpha1/user_types.go index faf317660..742f055d9 100644 --- a/api/v1alpha1/user_types.go +++ b/api/v1alpha1/user_types.go @@ -42,6 +42,18 @@ type UserResourceSpec struct { // enabled defines whether a user is enabled or disabled // +optional Enabled *bool `json:"enabled,omitempty"` + + // password is the password set for the user + // +optional + Password *PasswordSpec `json:"password,omitempty"` +} + +// +kubebuilder:validation:MinProperties:=1 +// +kubebuilder:validation:MaxProperties:=1 +type PasswordSpec struct { + // secretRef is a reference to a Secret containing the password for this user. + // +optional + SecretRef *KubernetesNameRef `json:"secretRef,omitempty"` } // UserFilter defines an existing resource by its properties @@ -81,4 +93,9 @@ type UserResourceStatus struct { // enabled defines whether a user is enabled or disabled // +optional Enabled bool `json:"enabled,omitempty"` + + // passwordExpiresAt filters the response based on expriing passwords. + // +kubebuilder:validation:MaxLength:=255 + // +optional + PasswordExpiresAt string `json:"passwordExpiresAt,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 4bd75eca4..1ae2be5a7 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2786,6 +2786,26 @@ func (in *NeutronStatusMetadata) DeepCopy() *NeutronStatusMetadata { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PasswordSpec) DeepCopyInto(out *PasswordSpec) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(KubernetesNameRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordSpec. +func (in *PasswordSpec) DeepCopy() *PasswordSpec { + if in == nil { + return nil + } + out := new(PasswordSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Port) DeepCopyInto(out *Port) { *out = *in @@ -5924,6 +5944,11 @@ func (in *UserResourceSpec) DeepCopyInto(out *UserResourceSpec) { *out = new(bool) **out = **in } + if in.Password != nil { + in, out := &in.Password, &out.Password + *out = new(PasswordSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserResourceSpec. diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index 9250f4e36..49a18ab83 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -127,6 +127,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.NetworkSpec": schema_openstack_resource_controller_v2_api_v1alpha1_NetworkSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.NetworkStatus": schema_openstack_resource_controller_v2_api_v1alpha1_NetworkStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.NeutronStatusMetadata": schema_openstack_resource_controller_v2_api_v1alpha1_NeutronStatusMetadata(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.PasswordSpec": schema_openstack_resource_controller_v2_api_v1alpha1_PasswordSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Port": schema_openstack_resource_controller_v2_api_v1alpha1_Port(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.PortFilter": schema_openstack_resource_controller_v2_api_v1alpha1_PortFilter(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.PortImport": schema_openstack_resource_controller_v2_api_v1alpha1_PortImport(ref), @@ -5235,6 +5236,25 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_NeutronStatusMetadata( } } +func schema_openstack_resource_controller_v2_api_v1alpha1_PasswordSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "secretRef": { + SchemaProps: spec.SchemaProps{ + Description: "secretRef is a reference to a Secret containing the password for this user.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_openstack_resource_controller_v2_api_v1alpha1_Port(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -11393,9 +11413,17 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_UserResourceSpec(ref c Format: "", }, }, + "password": { + SchemaProps: spec.SchemaProps{ + Description: "password is the password set for the user", + Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.PasswordSpec"), + }, + }, }, }, }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.PasswordSpec"}, } } @@ -11441,6 +11469,13 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_UserResourceStatus(ref Format: "", }, }, + "passwordExpiresAt": { + SchemaProps: spec.SchemaProps{ + Description: "passwordExpiresAt filters the response based on expriing passwords.", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, diff --git a/config/crd/bases/openstack.k-orc.cloud_users.yaml b/config/crd/bases/openstack.k-orc.cloud_users.yaml index bc8835301..ec3101c1a 100644 --- a/config/crd/bases/openstack.k-orc.cloud_users.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_users.yaml @@ -186,6 +186,18 @@ spec: minLength: 1 pattern: ^[^,]+$ type: string + password: + description: password is the password set for the user + maxProperties: 1 + minProperties: 1 + properties: + secretRef: + description: secretRef is a reference to a Secret containing + the password for this user. + maxLength: 253 + minLength: 1 + type: string + type: object type: object required: - cloudCredentialsRef @@ -313,6 +325,11 @@ spec: not be unique. maxLength: 1024 type: string + passwordExpiresAt: + description: passwordExpiresAt filters the response based on expriing + passwords. + maxLength: 255 + type: string type: object type: object required: diff --git a/config/samples/openstack_v1alpha1_user.yaml b/config/samples/openstack_v1alpha1_user.yaml index 09067e614..d27169d30 100644 --- a/config/samples/openstack_v1alpha1_user.yaml +++ b/config/samples/openstack_v1alpha1_user.yaml @@ -1,12 +1,48 @@ --- apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: user-sample +spec: + cloudCredentialsRef: + cloudName: devstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: user-sample +spec: + cloudCredentialsRef: + cloudName: devstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- + apiVersion: v1 + kind: Secret + metadata: + name: user-sample + type: Opaque + stringData: + password: "TestPassword" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User metadata: name: user-sample spec: cloudCredentialsRef: - cloudName: openstack-admin + cloudName: devstack-admin secretName: openstack-clouds managementPolicy: managed resource: - description: Sample User + name: user-sample + description: User sample + domainRef: user-sample + defaultProjectRef: user-sample + enabled: true + password: + secretRef: user-sample \ No newline at end of file diff --git a/internal/controllers/user/actuator.go b/internal/controllers/user/actuator.go index 391205112..2d9c4338d 100644 --- a/internal/controllers/user/actuator.go +++ b/internal/controllers/user/actuator.go @@ -135,6 +135,27 @@ func (actuator userActuator) CreateResource(ctx context.Context, obj orcObjectPT defaultProjectID = ptr.Deref(project.Status.ID, "") } } + + var password string + if resource.Password != nil { + secret, secretReconcileStatus := dependency.FetchDependency( + ctx, actuator.k8sClient, obj.Namespace, + resource.Password.SecretRef, "Secret", + func(*corev1.Secret) bool { return true }, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(secretReconcileStatus) + if secretReconcileStatus == nil { + var ok bool + passwordBytes, ok := secret.Data["password"] + if !ok { + reconcileStatus = reconcileStatus.WithReconcileStatus( + progress.NewReconcileStatus().WithProgressMessage("Password secret does not contain \"password\" key")) + } else { + password = string(passwordBytes) + } + } + } + if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule { return nil, reconcileStatus } @@ -144,6 +165,7 @@ func (actuator userActuator) CreateResource(ctx context.Context, obj orcObjectPT DomainID: domainID, Enabled: resource.Enabled, DefaultProjectID: defaultProjectID, + Password: password, } osResource, err := actuator.osClient.CreateUser(ctx, createOpts) diff --git a/internal/controllers/user/controller.go b/internal/controllers/user/controller.go index 4e432c0c7..2f966f873 100644 --- a/internal/controllers/user/controller.go +++ b/internal/controllers/user/controller.go @@ -20,6 +20,7 @@ import ( "context" "errors" + corev1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -86,6 +87,17 @@ var domainImportDependency = dependency.NewDependency[*orcv1alpha1.UserList, *or }, ) +var passwordDependency = dependency.NewDependency[*orcv1alpha1.UserList, *corev1.Secret]( + "spec.resource.password.secretRef", + func(user *orcv1alpha1.User) []string { + resource := user.Spec.Resource + if resource == nil || resource.Password == nil || resource.Password.SecretRef == nil { + return nil + } + return []string{string(*resource.Password.SecretRef)} + }, +) + // SetupWithManager sets up the controller with the Manager. func (c userReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { log := ctrl.LoggerFrom(ctx) @@ -106,8 +118,14 @@ func (c userReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctr return err } + passwordWatchEventHandler, err := passwordDependency.WatchEventHandler(log, k8sClient) + if err != nil { + return err + } + builder := ctrl.NewControllerManagedBy(mgr). WithOptions(options). + For(&orcv1alpha1.User{}). Watches(&orcv1alpha1.Domain{}, domainWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Domain{})), ). @@ -118,12 +136,14 @@ func (c userReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctr Watches(&orcv1alpha1.Domain{}, domainImportWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Domain{})), ). - For(&orcv1alpha1.User{}) + // General watch on secrets. + Watches(&corev1.Secret{}, passwordWatchEventHandler) if err := errors.Join( domainDependency.AddToManager(ctx, mgr), projectDependency.AddToManager(ctx, mgr), domainImportDependency.AddToManager(ctx, mgr), + passwordDependency.AddToManager(ctx, mgr), credentialsDependency.AddToManager(ctx, mgr), credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency), ); err != nil { diff --git a/internal/controllers/user/tests/user-create-full/00-create-resource.yaml b/internal/controllers/user/tests/user-create-full/00-create-resource.yaml index 4df449bda..e4f5f132f 100644 --- a/internal/controllers/user/tests/user-create-full/00-create-resource.yaml +++ b/internal/controllers/user/tests/user-create-full/00-create-resource.yaml @@ -20,6 +20,14 @@ spec: secretName: openstack-clouds managementPolicy: managed resource: {} +--- + apiVersion: v1 + kind: Secret + metadata: + name: user-create-full + type: Opaque + stringData: + password: "TestPassword" --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User @@ -35,4 +43,6 @@ spec: description: User from "create full" test domainRef: user-create-full defaultProjectRef: user-create-full - enabled: true \ No newline at end of file + enabled: true + password: + secretRef: user-create-full \ No newline at end of file diff --git a/internal/controllers/user/tests/user-create-minimal/00-assert.yaml b/internal/controllers/user/tests/user-create-minimal/00-assert.yaml index 950d429bd..02a409a8f 100644 --- a/internal/controllers/user/tests/user-create-minimal/00-assert.yaml +++ b/internal/controllers/user/tests/user-create-minimal/00-assert.yaml @@ -27,3 +27,5 @@ assertAll: - celExpr: "!has(user.status.resource.description)" - celExpr: "user.status.resource.domainID == 'default'" - celExpr: "!has(user.status.resource.defaultProjectID)" + - celExpr: "!has(user.status.resource.passwordExpiresAt)" + diff --git a/internal/controllers/user/tests/user-dependency/01-create-dependencies.yaml b/internal/controllers/user/tests/user-dependency/01-create-dependencies.yaml index 4a292db93..d47398d55 100644 --- a/internal/controllers/user/tests/user-dependency/01-create-dependencies.yaml +++ b/internal/controllers/user/tests/user-dependency/01-create-dependencies.yaml @@ -25,4 +25,12 @@ spec: cloudName: openstack-admin secretName: openstack-clouds managementPolicy: managed - resource: {} \ No newline at end of file + resource: {} +--- +apiVersion: v1 +kind: Secret +metadata: + name: user-create-full +type: Opaque +stringData: + password: "TestPassword" \ No newline at end of file diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/passwordspec.go b/pkg/clients/applyconfiguration/api/v1alpha1/passwordspec.go new file mode 100644 index 000000000..4dd652577 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/passwordspec.go @@ -0,0 +1,43 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" +) + +// PasswordSpecApplyConfiguration represents a declarative configuration of the PasswordSpec type for use +// with apply. +type PasswordSpecApplyConfiguration struct { + SecretRef *apiv1alpha1.KubernetesNameRef `json:"secretRef,omitempty"` +} + +// PasswordSpecApplyConfiguration constructs a declarative configuration of the PasswordSpec type for use with +// apply. +func PasswordSpec() *PasswordSpecApplyConfiguration { + return &PasswordSpecApplyConfiguration{} +} + +// WithSecretRef sets the SecretRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SecretRef field is set to the value of the last call. +func (b *PasswordSpecApplyConfiguration) WithSecretRef(value apiv1alpha1.KubernetesNameRef) *PasswordSpecApplyConfiguration { + b.SecretRef = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go index ed4b86a2e..e7898082e 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go @@ -25,11 +25,12 @@ import ( // UserResourceSpecApplyConfiguration represents a declarative configuration of the UserResourceSpec type for use // with apply. type UserResourceSpecApplyConfiguration struct { - Name *apiv1alpha1.OpenStackName `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"` - DefaultProjectRef *apiv1alpha1.KubernetesNameRef `json:"defaultProjectRef,omitempty"` - Enabled *bool `json:"enabled,omitempty"` + Name *apiv1alpha1.OpenStackName `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"` + DefaultProjectRef *apiv1alpha1.KubernetesNameRef `json:"defaultProjectRef,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Password *PasswordSpecApplyConfiguration `json:"password,omitempty"` } // UserResourceSpecApplyConfiguration constructs a declarative configuration of the UserResourceSpec type for use with @@ -77,3 +78,11 @@ func (b *UserResourceSpecApplyConfiguration) WithEnabled(value bool) *UserResour b.Enabled = &value return b } + +// WithPassword sets the Password field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Password field is set to the value of the last call. +func (b *UserResourceSpecApplyConfiguration) WithPassword(value *PasswordSpecApplyConfiguration) *UserResourceSpecApplyConfiguration { + b.Password = value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/userresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/userresourcestatus.go index 05093ff79..c23b0b6cf 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/userresourcestatus.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/userresourcestatus.go @@ -21,11 +21,12 @@ package v1alpha1 // UserResourceStatusApplyConfiguration represents a declarative configuration of the UserResourceStatus type for use // with apply. type UserResourceStatusApplyConfiguration struct { - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - DomainID *string `json:"domainID,omitempty"` - DefaultProjectID *string `json:"defaultProjectID,omitempty"` - Enabled *bool `json:"enabled,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + DomainID *string `json:"domainID,omitempty"` + DefaultProjectID *string `json:"defaultProjectID,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + PasswordExpiresAt *string `json:"passwordExpiresAt,omitempty"` } // UserResourceStatusApplyConfiguration constructs a declarative configuration of the UserResourceStatus type for use with @@ -73,3 +74,11 @@ func (b *UserResourceStatusApplyConfiguration) WithEnabled(value bool) *UserReso b.Enabled = &value return b } + +// WithPasswordExpiresAt sets the PasswordExpiresAt field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PasswordExpiresAt field is set to the value of the last call. +func (b *UserResourceStatusApplyConfiguration) WithPasswordExpiresAt(value string) *UserResourceStatusApplyConfiguration { + b.PasswordExpiresAt = &value + return b +} diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index 216436517..8f33f6ae4 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -1450,6 +1450,12 @@ var schemaYAML = typed.YAMLObject(`types: - name: resource type: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.NetworkResourceStatus +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.PasswordSpec + map: + fields: + - name: secretRef + type: + scalar: string - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.Port map: fields: @@ -3409,6 +3415,9 @@ var schemaYAML = typed.YAMLObject(`types: - name: name type: scalar: string + - name: password + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.PasswordSpec - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.UserResourceStatus map: fields: @@ -3427,6 +3436,9 @@ var schemaYAML = typed.YAMLObject(`types: - name: name type: scalar: string + - name: passwordExpiresAt + type: + scalar: string - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.UserSpec map: fields: diff --git a/pkg/clients/applyconfiguration/utils.go b/pkg/clients/applyconfiguration/utils.go index bb76ee624..605bc9ace 100644 --- a/pkg/clients/applyconfiguration/utils.go +++ b/pkg/clients/applyconfiguration/utils.go @@ -208,6 +208,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1alpha1.NetworkStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("NeutronStatusMetadata"): return &apiv1alpha1.NeutronStatusMetadataApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("PasswordSpec"): + return &apiv1alpha1.PasswordSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Port"): return &apiv1alpha1.PortApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("PortFilter"): diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index 2513429b8..6529bc4de 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -1925,6 +1925,7 @@ _Appears in:_ - [HostID](#hostid) - [NetworkFilter](#networkfilter) - [NetworkResourceSpec](#networkresourcespec) +- [PasswordSpec](#passwordspec) - [PortFilter](#portfilter) - [PortResourceSpec](#portresourcespec) - [RoleFilter](#rolefilter) @@ -2363,6 +2364,24 @@ _Appears in:_ +#### PasswordSpec + + + + + +_Validation:_ +- MaxProperties: 1 +- MinProperties: 1 + +_Appears in:_ +- [UserResourceSpec](#userresourcespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `secretRef` _[KubernetesNameRef](#kubernetesnameref)_ | secretRef is a reference to a Secret containing the password for this user. | | MaxLength: 253
MinLength: 1
| + + #### Port @@ -4436,6 +4455,7 @@ _Appears in:_ | `domainRef` _[KubernetesNameRef](#kubernetesnameref)_ | domainRef is a reference to the ORC Domain which this resource is associated with. | | MaxLength: 253
MinLength: 1
| | `defaultProjectRef` _[KubernetesNameRef](#kubernetesnameref)_ | defaultProjectRef is a reference to the Default Project which this resource is associated with. | | MaxLength: 253
MinLength: 1
| | `enabled` _boolean_ | enabled defines whether a user is enabled or disabled | | | +| `password` _[PasswordSpec](#passwordspec)_ | password is the password set for the user | | MaxProperties: 1
MinProperties: 1
| #### UserResourceStatus @@ -4456,6 +4476,7 @@ _Appears in:_ | `domainID` _string_ | domainID is the ID of the Domain to which the resource is associated. | | MaxLength: 1024
| | `defaultProjectID` _string_ | defaultProjectID is the ID of the Default Project to which the user is associated with. | | MaxLength: 1024
| | `enabled` _boolean_ | enabled defines whether a user is enabled or disabled | | | +| `passwordExpiresAt` _string_ | passwordExpiresAt filters the response based on expriing passwords. | | MaxLength: 255
| #### UserSpec