This is an automated email from the ASF dual-hosted git repository.

roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 3893386df3 [#12773] feat(idp-basic): add enabled flag for local IdP 
users (#12774)
3893386df3 is described below

commit 3893386df3f780966351389a0f79adef9737e36c
Author: MaSai <[email protected]>
AuthorDate: Wed Sep 2 14:05:12 2026 +0800

    [#12773] feat(idp-basic): add enabled flag for local IdP users (#12774)
    
    ### What changes were proposed in this pull request?
    
    - Add `enabled` to `idp_user_meta` (MySQL/H2/PostgreSQL 2.0.0 schema and
    1.3.0 → 2.0.0 upgrades).
    - Thread the flag through PO, SQL providers, service, model, DTO, REST,
    OpenAPI, and docs.
    - Create user: optional `enabled`, default `true`. Update: existing `PUT
    /api/idp/users/{user}` accepts `password` and/or `enabled` (at least one
    required) via `UpdateUserRequest`.
    - Disabled users cannot authenticate (same unauthorized message as a bad
    password).
    - Users listed in `gravitino.authorization.serviceAdmins` cannot be
    created disabled or disabled later.
    - Do not add `external_id` to IdP tables. Do not change `idp_group_meta`
    fields or add a group comment update API.
    
    ### Why are the changes needed?
    
    Metalake `user_meta` already has `enabled`. Local HTTP Basic identities
    need the same control so an administrator can lock a user out without
    deleting the account. Service admins must stay enabled so management
    APIs cannot be locked out.
    
    Fix: #12773
    
    ### Does this PR introduce _any_ user-facing change?
    
    - Yes. `POST /api/idp/users` accepts optional `enabled` (default
    `true`).
    - `PUT /api/idp/users/{user}` accepts `password` and/or `enabled`.
    - User responses include `enabled`.
    - Disabling a service admin returns 400.
    
    ### How was this patch tested?
    
    - `./gradlew :plugins:idp-basic:test -PskipITs`
    - `./gradlew :docs:build`
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 docs/open-api/idp/idp.yaml                         | 55 ++++++++++++++-----
 docs/security/local-users-and-groups.md            | 16 +++---
 .../apache/gravitino/idp/IdpUserGroupManager.java  | 62 ++++++++++++++++++++--
 .../org/apache/gravitino/idp/dto/IdpUserDTO.java   | 14 ++++-
 .../gravitino/idp/dto/requests/AddUserRequest.java | 26 ++++++++-
 ...PasswordRequest.java => UpdateUserRequest.java} | 43 +++++++++++----
 .../org/apache/gravitino/idp/model/IdpUser.java    | 53 ++++++++++++++----
 .../idp/storage/mapper/IdpUserMetaMapper.java      |  4 ++
 .../mapper/IdpUserMetaSQLProviderFactory.java      |  5 ++
 .../provider/base/IdpUserMetaBaseSQLProvider.java  | 17 +++++-
 .../mapper/provider/h2/IdpUserMetaH2Provider.java  |  3 +-
 .../postgresql/IdpUserMetaPostgreSQLProvider.java  |  3 +-
 .../apache/gravitino/idp/storage/po/IdpUserPO.java |  1 +
 .../idp/storage/po/IdpUserWithGroupsPO.java        |  1 +
 .../storage/relational/utils/IdpPOConverters.java  |  5 +-
 .../idp/storage/service/IdpUserMetaService.java    | 25 +++++++++
 .../gravitino/idp/web/rest/IdpUserOperations.java  | 19 ++++---
 .../gravitino/idp/TestIdpUserGroupManager.java     | 52 ++++++++++++++++++
 .../apache/gravitino/idp/dto/TestIdpUserDTO.java   |  6 ++-
 .../idp/dto/requests/TestAddUserRequest.java       |  2 +
 ...wordRequest.java => TestUpdateUserRequest.java} | 43 +++++++++------
 .../idp/integration/test/IdpRESTApiIT.java         | 19 +++++--
 .../gravitino/idp/storage/po/TestIdpUserPO.java    |  1 +
 .../storage/service/TestIdpUserMetaService.java    | 19 +++++++
 .../gravitino/idp/web/rest/TestIdpOperations.java  | 31 +++++++++--
 scripts/h2/schema-2.0.0-h2.sql                     |  1 +
 scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql           |  2 +
 scripts/mysql/schema-2.0.0-mysql.sql               |  1 +
 scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql     |  3 ++
 scripts/postgresql/schema-2.0.0-postgresql.sql     |  2 +
 .../upgrade-1.3.0-to-2.0.0-postgresql.sql          |  3 ++
 31 files changed, 450 insertions(+), 87 deletions(-)

diff --git a/docs/open-api/idp/idp.yaml b/docs/open-api/idp/idp.yaml
index 23ac9e7dcb..42d4a05749 100644
--- a/docs/open-api/idp/idp.yaml
+++ b/docs/open-api/idp/idp.yaml
@@ -109,21 +109,26 @@ paths:
     put:
       tags:
         - IDP
-      summary: Change local user store user password
-      description: Updates the password of the specified local user store user 
and returns the user object with unchanged group membership.
-      operationId: changeIdpUserPassword
+      summary: Update local user store user
+      description: >
+        Updates the password and/or enabled flag of the specified local user 
store user and returns
+        the user object. At least one of `password` or `enabled` is required. 
Disabled users cannot
+        authenticate. Users listed in `gravitino.authorization.serviceAdmins` 
cannot be disabled.
+      operationId: updateIdpUser
       requestBody:
         required: true
         content:
           application/json:
             schema:
-              $ref: "#/components/schemas/ChangePasswordRequest"
+              $ref: "#/components/schemas/UpdateUserRequest"
             examples:
-              ChangePasswordRequest:
-                $ref: "#/components/examples/ChangePasswordRequest"
+              UpdateUserPasswordRequest:
+                $ref: "#/components/examples/UpdateUserPasswordRequest"
+              UpdateUserEnabledRequest:
+                $ref: "#/components/examples/UpdateUserEnabledRequest"
       responses:
         "200":
-          description: Returns the local user store user after the password 
change
+          description: Returns the local user store user after the update
           content:
             application/vnd.gravitino.v1+json:
               schema:
@@ -139,7 +144,7 @@ paths:
                 $ref: "../openapi.yaml#/components/schemas/ErrorModel"
               examples:
                 IllegalArgumentException:
-                  $ref: 
"#/components/examples/IdpChangePasswordIllegalArgumentException"
+                  $ref: 
"#/components/examples/IdpUpdateUserIllegalArgumentException"
         "403":
           $ref: "#/components/responses/IdpForbiddenErrorResponse"
         "404":
@@ -341,6 +346,10 @@ components:
           type: string
           maxLength: 128
           description: The username of the local user store user
+        enabled:
+          type: boolean
+          default: true
+          description: Whether the local user store user is enabled. Disabled 
users cannot authenticate.
         groups:
           type: array
           items:
@@ -387,11 +396,18 @@ components:
           maxLength: 64
           description: The password of the local user store user to add.
           writeOnly: true
+        enabled:
+          type: boolean
+          default: true
+          description: Whether the user is enabled. Disabled users cannot 
authenticate.
 
-    ChangePasswordRequest:
+    UpdateUserRequest:
       type: object
-      required:
-        - password
+      anyOf:
+        - required:
+            - password
+        - required:
+            - enabled
       properties:
         password:
           type: string
@@ -400,6 +416,9 @@ components:
           maxLength: 64
           description: The new password of the local user store user.
           writeOnly: true
+        enabled:
+          type: boolean
+          description: Whether the local user store user should be enabled.
 
     AddGroupRequest:
       type: object
@@ -486,11 +505,16 @@ components:
         "password": "Passw0rd-1234"
       }
 
-    ChangePasswordRequest:
+    UpdateUserPasswordRequest:
       value: {
         "password": "Passw0rd-5678"
       }
 
+    UpdateUserEnabledRequest:
+      value: {
+        "enabled": false
+      }
+
     AddGroupRequest:
       value: {
         "group": "engineers",
@@ -508,6 +532,7 @@ components:
         "code": 0,
         "user": {
           "name": "alice",
+          "enabled": true,
           "groups": []
         }
       }
@@ -517,6 +542,7 @@ components:
         "code": 0,
         "user": {
           "name": "alice",
+          "enabled": true,
           "groups": ["engineers"]
         }
       }
@@ -526,6 +552,7 @@ components:
         "code": 0,
         "user": {
           "name": "alice",
+          "enabled": true,
           "groups": ["engineers"]
         }
       }
@@ -567,11 +594,11 @@ components:
         "message": "Failed to operate local user store user [] operation 
[ADD], reason [\"user\" field is required and cannot be empty]"
       }
 
-    IdpChangePasswordIllegalArgumentException:
+    IdpUpdateUserIllegalArgumentException:
       value: {
         "code": 1001,
         "type": "IllegalArgumentException",
-        "message": "Failed to operate local user store user [alice] operation 
[UPDATE], reason [\"password\" field is required and cannot be empty]"
+        "message": "Failed to operate local user store user [alice] operation 
[UPDATE], reason [\"password\" or \"enabled\" field is required]"
       }
 
     IdpAddGroupIllegalArgumentException:
diff --git a/docs/security/local-users-and-groups.md 
b/docs/security/local-users-and-groups.md
index 4d661358bb..c497b59a8b 100644
--- a/docs/security/local-users-and-groups.md
+++ b/docs/security/local-users-and-groups.md
@@ -81,14 +81,14 @@ All management endpoints are under 
`http://{host}:{port}/api/idp` and require Ba
 
 ### User Operations
 
-| Operation       | Method | Path                     | Body                   
                     |
-|-----------------|--------|--------------------------|---------------------------------------------|
-| Get a user      | GET    | `/api/idp/users/{user}`  | None                   
                     |
-| Add a user      | POST   | `/api/idp/users`         | 
`{"user":"alice","password":"{password}"}`  |
-| Reset a password| PUT    | `/api/idp/users/{user}`  | 
`{"password":"{new_password}"}`             |
-| Remove a user   | DELETE | `/api/idp/users/{user}`  | None                   
                     |
+| Operation                | Method | Path                                | 
Body                                       |
+|--------------------------|--------|-------------------------------------|--------------------------------------------|
+| Get a user               | GET    | `/api/idp/users/{user}`             | 
None                                       |
+| Add a user               | POST   | `/api/idp/users`                    | 
`{"user":"alice","password":"{password}"}` |
+| Update a user            | PUT    | `/api/idp/users/{user}`             | 
`{"password":"{new_password}"}` and/or `{"enabled":false}` |
+| Remove a user            | DELETE | `/api/idp/users/{user}`             | 
None                                       |
 
-The add-user body uses the field name `user` rather than `name`.
+The add-user body uses the field name `user` rather than `name`. `enabled` is 
optional on create and defaults to `true`. A disabled user cannot authenticate. 
`PUT /api/idp/users/{user}` accepts `password` and/or `enabled`; at least one 
is required. Users listed in `gravitino.authorization.serviceAdmins` cannot be 
disabled.
 
 ```shell
 curl -s -X POST -H "Accept: application/vnd.gravitino.v1+json" \
@@ -107,7 +107,7 @@ curl -s -X POST -H "Accept: 
application/vnd.gravitino.v1+json" \
 | Remove a group           | DELETE | `/api/idp/groups/{group}?force={true 
false}`| None                                                       |
 | Change group membership  | PUT    | `/api/idp/groups/{group}/users`          
   | `{"usersToAdd":["alice"],"usersToRemove":["carol"]}`       |
 
-The add-group body uses the field name `group` rather than `name`. `comment` 
is optional (max 1024 characters, utf8mb4) and stored as the group description. 
Removing a group that still has members fails unless `force=true`. A membership 
change requires at least one of `usersToAdd` or `usersToRemove`, and accepts 
both in a single request.
+The add-group body uses the field name `group` rather than `name`. `comment` 
is optional on create (max 1024 characters, utf8mb4) and stored as the group 
description. Removing a group that still has members fails unless `force=true`. 
A membership change requires at least one of `usersToAdd` or `usersToRemove`, 
and accepts both in a single request.
 
 ```shell
 curl -s -X PUT -H "Accept: application/vnd.gravitino.v1+json" \
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
index c4f89ad4ca..7fcc858699 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
@@ -56,6 +56,7 @@ public class IdpUserGroupManager implements Closeable {
 
   private static volatile IdpUserGroupManager instance;
 
+  private final Config config;
   private final IdpRelationalStorage relationalStorage;
   private final IdpGarbageCollector garbageCollector;
   private final IdGenerator idGenerator;
@@ -80,6 +81,7 @@ public class IdpUserGroupManager implements Closeable {
   }
 
   private IdpUserGroupManager(Config config, IdGenerator idGenerator) {
+    this.config = config;
     this.relationalStorage = new IdpRelationalStorage(config);
     this.idGenerator = idGenerator;
     this.passwordHasher = PasswordHasherFactory.create();
@@ -113,7 +115,7 @@ public class IdpUserGroupManager implements Closeable {
     IdpCredentialValidator.validatePassword(initialAdminPassword);
     String passwordHash = passwordHasher.hash(initialAdminPassword);
     for (String serviceAdmin : missingServiceAdmins) {
-      USER_SERVICE.insertIdpUser(newUserPO(serviceAdmin, passwordHash));
+      USER_SERVICE.insertIdpUser(newUserPO(serviceAdmin, passwordHash, true));
     }
   }
 
@@ -125,9 +127,24 @@ public class IdpUserGroupManager implements Closeable {
    * @return The created built-in IdP user.
    */
   public IdpUser addUser(String username, String password) throws IOException {
+    return addUser(username, password, true);
+  }
+
+  /**
+   * Adds a built-in IdP user.
+   *
+   * @param username The username.
+   * @param password The plaintext password.
+   * @param enabled Whether the user is enabled.
+   * @return The created built-in IdP user.
+   * @throws IllegalArgumentException if {@code enabled} is {@code false} and 
the username is a
+   *     configured service admin
+   */
+  public IdpUser addUser(String username, String password, boolean enabled) 
throws IOException {
+    checkCanDisableUser(username, enabled);
     String passwordHash = passwordHasher.hash(password);
-    USER_SERVICE.insertIdpUser(newUserPO(username, passwordHash));
-    return new IdpUser(username, Collections.emptyList());
+    USER_SERVICE.insertIdpUser(newUserPO(username, passwordHash, enabled));
+    return new IdpUser(username, Collections.emptyList(), enabled);
   }
 
   /**
@@ -153,7 +170,9 @@ public class IdpUserGroupManager implements Closeable {
   public IdpUser authenticate(String username, String password) {
     try {
       IdpUser user = USER_SERVICE.getIdpUser(username);
-      if (user.passwordHash() == null || !passwordHasher.verify(password, 
user.passwordHash())) {
+      if (!user.enabled()
+          || user.passwordHash() == null
+          || !passwordHasher.verify(password, user.passwordHash())) {
         throw new UnauthorizedException(
             "Invalid username or password", 
AuthConstants.AUTHORIZATION_BASIC_HEADER.trim());
       }
@@ -176,6 +195,21 @@ public class IdpUserGroupManager implements Closeable {
     return USER_SERVICE.updateIdpUserPassword(username, 
passwordHasher.hash(password));
   }
 
+  /**
+   * Enables or disables a built-in IdP user.
+   *
+   * @param username The username.
+   * @param enabled Whether the user should be enabled.
+   * @return {@code true} if the enabled flag was updated
+   * @throws NotFoundException if the user does not exist
+   * @throws IllegalArgumentException if {@code enabled} is {@code false} and 
the username is a
+   *     configured service admin
+   */
+  public boolean updateEnabled(String username, boolean enabled) {
+    checkCanDisableUser(username, enabled);
+    return USER_SERVICE.updateIdpUserEnabled(username, enabled);
+  }
+
   /**
    * Adds a built-in IdP group.
    *
@@ -247,11 +281,29 @@ public class IdpUserGroupManager implements Closeable {
     relationalStorage.close();
   }
 
-  private IdpUserPO newUserPO(String username, String passwordHash) {
+  private void checkCanDisableUser(String username, boolean enabled) {
+    if (enabled) {
+      return;
+    }
+    Preconditions.checkArgument(
+        !isServiceAdmin(username), "Cannot disable service admin %s", 
username);
+  }
+
+  private boolean isServiceAdmin(String username) {
+    String rawServiceAdmins = 
config.getRawString(Configs.SERVICE_ADMINS.getKey());
+    if (StringUtils.isBlank(rawServiceAdmins)) {
+      return false;
+    }
+    List<String> serviceAdmins = config.get(Configs.SERVICE_ADMINS);
+    return serviceAdmins != null && serviceAdmins.contains(username);
+  }
+
+  private IdpUserPO newUserPO(String username, String passwordHash, boolean 
enabled) {
     return IdpUserPO.builder()
         .withUserId(idGenerator.nextId())
         .withUsername(username)
         .withPasswordHash(passwordHash)
+        .withEnabled(enabled)
         .withCurrentVersion(POConverters.INIT_VERSION)
         .withLastVersion(POConverters.INIT_VERSION)
         .withDeletedAt(POConverters.DEFAULT_DELETED_AT)
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpUserDTO.java 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpUserDTO.java
index 80ec5c4e76..b23df5f20d 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpUserDTO.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpUserDTO.java
@@ -41,6 +41,9 @@ public class IdpUserDTO {
   @JsonProperty("name")
   private String name;
 
+  @JsonProperty("enabled")
+  private boolean enabled = true;
+
   @JsonProperty("groups")
   @JsonSetter(nulls = Nulls.AS_EMPTY)
   private List<String> groups = Collections.emptyList();
@@ -49,10 +52,11 @@ public class IdpUserDTO {
    * Creates a new instance of IdpUserDTO.
    *
    * @param name The name of the built-in IdP user DTO.
+   * @param enabled Whether the built-in IdP user is enabled.
    * @param groups The groups of the built-in IdP user DTO.
    */
   @Builder(setterPrefix = "with")
-  protected IdpUserDTO(String name, List<String> groups) {
+  protected IdpUserDTO(String name, Boolean enabled, List<String> groups) {
     Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot be 
null or empty");
     if (groups != null) {
       groups.forEach(
@@ -62,6 +66,7 @@ public class IdpUserDTO {
                   "groups cannot contain null or empty group names"));
     }
     this.name = name;
+    this.enabled = enabled == null || enabled;
     this.groups = groups == null ? Collections.emptyList() : groups;
   }
 
@@ -72,6 +77,13 @@ public class IdpUserDTO {
     return name;
   }
 
+  /**
+   * @return Whether the built-in IdP user is enabled.
+   */
+  public boolean enabled() {
+    return enabled;
+  }
+
   /**
    * The groups of the built-in IdP user. A user can belong to multiple groups.
    *
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/requests/AddUserRequest.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/requests/AddUserRequest.java
index 6be8ade145..9f2b78fe9b 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/requests/AddUserRequest.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/requests/AddUserRequest.java
@@ -43,9 +43,12 @@ public class AddUserRequest implements RESTRequest {
   @ToString.Exclude
   private final String password;
 
+  @JsonProperty("enabled")
+  private final Boolean enabled;
+
   /** Default constructor for AddUserRequest. (Used for Jackson 
deserialization.) */
   public AddUserRequest() {
-    this(null, null);
+    this(null, null, null);
   }
 
   /**
@@ -55,9 +58,21 @@ public class AddUserRequest implements RESTRequest {
    * @param password The password of the built-in IdP user.
    */
   public AddUserRequest(String user, String password) {
+    this(user, password, null);
+  }
+
+  /**
+   * Creates a new AddUserRequest.
+   *
+   * @param user The user name of the built-in IdP user.
+   * @param password The password of the built-in IdP user.
+   * @param enabled Whether the built-in IdP user is enabled. Null defaults to 
{@code true}.
+   */
+  public AddUserRequest(String user, String password, Boolean enabled) {
     super();
     this.user = user;
     this.password = password;
+    this.enabled = enabled;
   }
 
   /**
@@ -70,4 +85,13 @@ public class AddUserRequest implements RESTRequest {
     IdpCredentialValidator.validateUsername(user);
     IdpCredentialValidator.validatePassword(password);
   }
+
+  /**
+   * Returns whether the user should be enabled, defaulting to {@code true} 
when omitted.
+   *
+   * @return Whether the user is enabled.
+   */
+  public boolean enabledOrDefault() {
+    return enabled == null || enabled;
+  }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/requests/ChangePasswordRequest.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/requests/UpdateUserRequest.java
similarity index 54%
rename from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/requests/ChangePasswordRequest.java
rename to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/requests/UpdateUserRequest.java
index f923585adf..6414d0c603 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/requests/ChangePasswordRequest.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/requests/UpdateUserRequest.java
@@ -7,7 +7,7 @@
  * "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
+ * 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
@@ -19,7 +19,9 @@
 
 package org.apache.gravitino.idp.dto.requests;
 
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
 import lombok.Builder;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -28,40 +30,61 @@ import lombok.extern.jackson.Jacksonized;
 import org.apache.gravitino.idp.basic.IdpCredentialValidator;
 import org.apache.gravitino.rest.RESTRequest;
 
-/** Represents a request to change a built-in IdP user password. */
+/** Represents a request to update a built-in IdP user password and/or enabled 
flag. */
 @Getter
 @EqualsAndHashCode
 @ToString
 @Builder
 @Jacksonized
-public class ChangePasswordRequest implements RESTRequest {
+public class UpdateUserRequest implements RESTRequest {
 
   @JsonProperty("password")
   @ToString.Exclude
   private final String password;
 
-  /** Default constructor for ChangePasswordRequest. (Used for Jackson 
deserialization.) */
-  public ChangePasswordRequest() {
-    this(null);
+  @JsonProperty("enabled")
+  @JsonInclude(JsonInclude.Include.NON_NULL)
+  private final Boolean enabled;
+
+  /** Default constructor for UpdateUserRequest. (Used for Jackson 
deserialization.) */
+  public UpdateUserRequest() {
+    this(null, null);
   }
 
   /**
-   * Creates a new ChangePasswordRequest.
+   * Creates a new UpdateUserRequest that updates only the password.
    *
    * @param password The new password of the built-in IdP user.
    */
-  public ChangePasswordRequest(String password) {
+  public UpdateUserRequest(String password) {
+    this(password, null);
+  }
+
+  /**
+   * Creates a new UpdateUserRequest.
+   *
+   * @param password The new password of the built-in IdP user, or {@code 
null} to leave it
+   *     unchanged.
+   * @param enabled Whether the built-in IdP user should be enabled, or {@code 
null} to leave it
+   *     unchanged.
+   */
+  public UpdateUserRequest(String password, Boolean enabled) {
     super();
     this.password = password;
+    this.enabled = enabled;
   }
 
   /**
-   * Validates the {@link ChangePasswordRequest} request.
+   * Validates the {@link UpdateUserRequest} request.
    *
    * @throws IllegalArgumentException If the request is invalid, this 
exception is thrown.
    */
   @Override
   public void validate() throws IllegalArgumentException {
-    IdpCredentialValidator.validatePassword(password);
+    Preconditions.checkArgument(
+        password != null || enabled != null, "\"password\" or \"enabled\" 
field is required");
+    if (password != null) {
+      IdpCredentialValidator.validatePassword(password);
+    }
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java
index 7fc84c2ea0..ae698f9b40 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java
@@ -29,6 +29,7 @@ public class IdpUser {
 
   private final String name;
   private final String passwordHash;
+  private final boolean enabled;
   private final List<String> groupNames;
 
   /**
@@ -38,9 +39,18 @@ public class IdpUser {
    * @param groupNames The group names the user belongs to.
    */
   public IdpUser(String name, List<String> groupNames) {
-    this.name = name;
-    this.passwordHash = null;
-    this.groupNames = groupNames;
+    this(name, null, groupNames, true);
+  }
+
+  /**
+   * Creates a built-in IdP user without a password hash.
+   *
+   * @param name The username.
+   * @param groupNames The group names the user belongs to.
+   * @param enabled Whether the user is enabled.
+   */
+  public IdpUser(String name, List<String> groupNames, boolean enabled) {
+    this(name, null, groupNames, enabled);
   }
 
   /**
@@ -51,10 +61,25 @@ public class IdpUser {
    * @param groupNames The group names the user belongs to.
    */
   public IdpUser(String name, String passwordHash, List<String> groupNames) {
-    Preconditions.checkArgument(
-        StringUtils.isNotBlank(passwordHash), "passwordHash must not be 
blank");
+    this(name, passwordHash, groupNames, true);
+  }
+
+  /**
+   * Creates a built-in IdP user with a password hash loaded from storage.
+   *
+   * @param name The username.
+   * @param passwordHash The password hash, or null when the hash is not 
loaded.
+   * @param groupNames The group names the user belongs to.
+   * @param enabled Whether the user is enabled.
+   */
+  public IdpUser(String name, String passwordHash, List<String> groupNames, 
boolean enabled) {
+    if (passwordHash != null) {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(passwordHash), "passwordHash must not be 
blank");
+    }
     this.name = name;
     this.passwordHash = passwordHash;
+    this.enabled = enabled;
     this.groupNames = groupNames;
   }
 
@@ -72,6 +97,15 @@ public class IdpUser {
     return passwordHash;
   }
 
+  /**
+   * Returns whether the user is enabled.
+   *
+   * @return True if the user is enabled, false otherwise.
+   */
+  public boolean enabled() {
+    return enabled;
+  }
+
   /** Returns the group names the user belongs to. */
   public List<String> groupNames() {
     return groupNames;
@@ -83,7 +117,7 @@ public class IdpUser {
    * @return the user DTO
    */
   public IdpUserDTO toDTO() {
-    return IdpUserDTO.builder().withName(name).withGroups(groupNames).build();
+    return 
IdpUserDTO.builder().withName(name).withEnabled(enabled).withGroups(groupNames).build();
   }
 
   @Override
@@ -95,18 +129,19 @@ public class IdpUser {
       return false;
     }
     IdpUser that = (IdpUser) other;
-    return Objects.equals(name, that.name)
+    return enabled == that.enabled
+        && Objects.equals(name, that.name)
         && Objects.equals(passwordHash, that.passwordHash)
         && Objects.equals(groupNames, that.groupNames);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(name, passwordHash, groupNames);
+    return Objects.hash(name, passwordHash, enabled, groupNames);
   }
 
   @Override
   public String toString() {
-    return "IdpUser{name='" + name + "', groupNames=" + groupNames + '}';
+    return "IdpUser{name='" + name + "', enabled=" + enabled + ", groupNames=" 
+ groupNames + '}';
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
index 79552780d4..55482f5697 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
@@ -55,6 +55,10 @@ public interface IdpUserMetaMapper {
   Integer updateIdpUserPassword(
       @Param("username") String username, @Param("passwordHash") String 
passwordHash);
 
+  @UpdateProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"updateIdpUserEnabled")
+  Integer updateIdpUserEnabled(
+      @Param("username") String username, @Param("enabled") boolean enabled);
+
   @UpdateProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"softDeleteIdpUser")
   Integer softDeleteIdpUser(@Param("username") String username);
 
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
index 01de6df97e..e165749311 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
@@ -66,6 +66,11 @@ public class IdpUserMetaSQLProviderFactory {
     return currentProvider().updateIdpUserPassword(username, passwordHash);
   }
 
+  public static String updateIdpUserEnabled(
+      @Param("username") String username, @Param("enabled") boolean enabled) {
+    return currentProvider().updateIdpUserEnabled(username, enabled);
+  }
+
   public static String softDeleteIdpUser(@Param("username") String username) {
     return currentProvider().softDeleteIdpUser(username);
   }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
index 14234d8766..306ce43439 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
@@ -30,6 +30,7 @@ import org.apache.ibatis.annotations.Param;
 public class IdpUserMetaBaseSQLProvider {
   public String selectIdpUser(@Param("username") String username) {
     return "SELECT user_id as userId, user_name as username, password_hash as 
passwordHash,"
+        + " enabled as enabled,"
         + " current_version as currentVersion,"
         + " last_version as lastVersion, deleted_at as deletedAt"
         + " FROM "
@@ -39,6 +40,7 @@ public class IdpUserMetaBaseSQLProvider {
 
   public String selectIdpUserWithGroups(@Param("username") String username) {
     return "SELECT u.user_name as name, u.password_hash as passwordHash,"
+        + " u.enabled as enabled,"
         + " COALESCE(JSON_ARRAYAGG(g.group_name), JSON_ARRAY()) as groupNames"
         + " FROM "
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
@@ -49,12 +51,13 @@ public class IdpUserMetaBaseSQLProvider {
         + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
         + " g ON g.group_id = r.group_id AND g.deleted_at = 0"
         + " WHERE u.user_name = #{username} AND u.deleted_at = 0"
-        + " GROUP BY u.user_id, u.user_name, u.password_hash";
+        + " GROUP BY u.user_id, u.user_name, u.password_hash, u.enabled";
   }
 
   public String selectIdpUsersByUsernames(@Param("usernames") List<String> 
usernames) {
     return "<script>"
         + "SELECT user_id as userId, user_name as username, password_hash as 
passwordHash,"
+        + " enabled as enabled,"
         + " current_version as currentVersion,"
         + " last_version as lastVersion, deleted_at as deletedAt"
         + " FROM "
@@ -70,11 +73,12 @@ public class IdpUserMetaBaseSQLProvider {
   public String insertIdpUser(@Param("userMeta") IdpUserPO userPO) {
     return "INSERT INTO "
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
-        + " (user_id, user_name, password_hash, current_version, last_version, 
deleted_at)"
+        + " (user_id, user_name, password_hash, enabled, current_version, 
last_version, deleted_at)"
         + " VALUES ("
         + " #{userMeta.userId},"
         + " #{userMeta.username},"
         + " #{userMeta.passwordHash},"
+        + " #{userMeta.enabled},"
         + " #{userMeta.currentVersion},"
         + " #{userMeta.lastVersion},"
         + " #{userMeta.deletedAt}"
@@ -90,6 +94,15 @@ public class IdpUserMetaBaseSQLProvider {
         + " AND deleted_at = 0";
   }
 
+  public String updateIdpUserEnabled(
+      @Param("username") String username, @Param("enabled") boolean enabled) {
+    return "UPDATE "
+        + IdpUserMetaMapper.IDP_USER_TABLE_NAME
+        + " SET enabled = #{enabled}"
+        + " WHERE user_name = #{username}"
+        + " AND deleted_at = 0";
+  }
+
   public String softDeleteIdpUser(@Param("username") String username) {
     return "UPDATE "
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
index 36b00a074a..3783ef0495 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
@@ -31,6 +31,7 @@ public class IdpUserMetaH2Provider extends 
IdpUserMetaBaseSQLProvider {
   @Override
   public String selectIdpUserWithGroups(@Param("username") String username) {
     return "SELECT u.user_name as name, u.password_hash as passwordHash,"
+        + " u.enabled as enabled,"
         + " '['"
         + " || COALESCE(GROUP_CONCAT('\"' || g.group_name || '\"'), '')"
         + " || ']' as groupNames"
@@ -43,7 +44,7 @@ public class IdpUserMetaH2Provider extends 
IdpUserMetaBaseSQLProvider {
         + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
         + " g ON g.group_id = r.group_id AND g.deleted_at = 0"
         + " WHERE u.user_name = #{username} AND u.deleted_at = 0"
-        + " GROUP BY u.user_id, u.user_name, u.password_hash";
+        + " GROUP BY u.user_id, u.user_name, u.password_hash, u.enabled";
   }
 
   @Override
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpUserMetaPostgreSQLProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpUserMetaPostgreSQLProvider.java
index ff9ea02f7c..02fa785490 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpUserMetaPostgreSQLProvider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpUserMetaPostgreSQLProvider.java
@@ -31,6 +31,7 @@ public class IdpUserMetaPostgreSQLProvider extends 
IdpUserMetaBaseSQLProvider {
   @Override
   public String selectIdpUserWithGroups(@Param("username") String username) {
     return "SELECT u.user_name as name, u.password_hash as passwordHash,"
+        + " u.enabled as enabled,"
         + " COALESCE(JSON_AGG(g.group_name), '[]'::json) as groupNames"
         + " FROM "
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
@@ -41,7 +42,7 @@ public class IdpUserMetaPostgreSQLProvider extends 
IdpUserMetaBaseSQLProvider {
         + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
         + " g ON g.group_id = r.group_id AND g.deleted_at = 0"
         + " WHERE u.user_name = #{username} AND u.deleted_at = 0"
-        + " GROUP BY u.user_id, u.user_name, u.password_hash";
+        + " GROUP BY u.user_id, u.user_name, u.password_hash, u.enabled";
   }
 
   @Override
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
index 2da098a481..7dfed4b3fb 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
@@ -36,6 +36,7 @@ public class IdpUserPO {
   private Long userId;
   private String username;
   private String passwordHash;
+  @Builder.Default private Boolean enabled = true;
   private Long currentVersion;
   private Long lastVersion;
   private Long deletedAt;
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserWithGroupsPO.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserWithGroupsPO.java
index 4938d780a7..04370011cf 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserWithGroupsPO.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserWithGroupsPO.java
@@ -39,5 +39,6 @@ import lombok.ToString;
 public class IdpUserWithGroupsPO {
   private String name;
   private String passwordHash;
+  @Builder.Default private Boolean enabled = true;
   private String groupNames;
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpPOConverters.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpPOConverters.java
index 66a43a5dcb..61aa06bbed 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpPOConverters.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpPOConverters.java
@@ -42,10 +42,11 @@ public final class IdpPOConverters {
   public static IdpUser fromIdpUserWithGroupsPO(IdpUserWithGroupsPO userPO) {
     Preconditions.checkNotNull(userPO, "userPO must not be null");
     List<String> groupNames = parseJsonStringList(userPO.getGroupNames());
+    boolean enabled = userPO.getEnabled() == null || userPO.getEnabled();
     if (StringUtils.isBlank(userPO.getPasswordHash())) {
-      return new IdpUser(userPO.getName(), groupNames);
+      return new IdpUser(userPO.getName(), groupNames, enabled);
     }
-    return new IdpUser(userPO.getName(), userPO.getPasswordHash(), groupNames);
+    return new IdpUser(userPO.getName(), userPO.getPasswordHash(), groupNames, 
enabled);
   }
 
   /**
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
index 78a42e1b15..79b8a43728 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
@@ -139,6 +139,31 @@ public class IdpUserMetaService {
         });
   }
 
+  /**
+   * Updates whether an active user is enabled. Succeeds when the user exists 
and the update is
+   * committed, even if the stored flag is already equal to {@code enabled}.
+   *
+   * @param username username of the user
+   * @param enabled whether the user should be enabled
+   * @return {@code true} if the enabled flag was updated
+   * @throws NotFoundException if the user does not exist or is soft-deleted
+   */
+  @Monitored(
+      metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+      baseMetricName = "updateIdpUserEnabled")
+  public boolean updateIdpUserEnabled(String username, boolean enabled) {
+    return SessionUtils.doWithCommitAndFetchResult(
+        IdpUserMetaMapper.class,
+        mapper -> {
+          IdpUserPO userPO = mapper.selectIdpUser(username);
+          if (userPO == null) {
+            throw new NotFoundException("IdP user not found: %s", username);
+          }
+          mapper.updateIdpUserEnabled(username, enabled);
+          return true;
+        });
+  }
+
   @Monitored(
       metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
       baseMetricName = "deleteIdpUserMetasByLegacyTimeline")
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpUserOperations.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpUserOperations.java
index 9d73e30b3c..31f7349807 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpUserOperations.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpUserOperations.java
@@ -34,7 +34,7 @@ import javax.ws.rs.core.Response;
 import org.apache.gravitino.dto.responses.RemoveResponse;
 import org.apache.gravitino.idp.IdpUserGroupManager;
 import org.apache.gravitino.idp.dto.requests.AddUserRequest;
-import org.apache.gravitino.idp.dto.requests.ChangePasswordRequest;
+import org.apache.gravitino.idp.dto.requests.UpdateUserRequest;
 import org.apache.gravitino.idp.dto.responses.IdpUserResponse;
 import org.apache.gravitino.idp.web.IdpManagement;
 import org.apache.gravitino.idp.web.IdpOperationType;
@@ -80,7 +80,9 @@ public class IdpUserOperations {
           request.validate();
           return IdpRESTUtils.ok(
               new IdpUserResponse(
-                  userGroupManager.addUser(request.getUser(), 
request.getPassword()).toDTO()));
+                  userGroupManager
+                      .addUser(request.getUser(), request.getPassword(), 
request.enabledOrDefault())
+                      .toDTO()));
         },
         "user",
         IdpOperationType.ADD,
@@ -90,14 +92,19 @@ public class IdpUserOperations {
   @PUT
   @Path("{user}")
   @Produces("application/vnd.gravitino.v1+json")
-  @Timed(name = "change-idp-user-password." + 
MetricNames.HTTP_PROCESS_DURATION, absolute = true)
-  @ResponseMetered(name = "change-idp-user-password", absolute = true)
-  public Response changePassword(@PathParam("user") String user, 
ChangePasswordRequest request) {
+  @Timed(name = "update-idp-user." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "update-idp-user", absolute = true)
+  public Response updateUser(@PathParam("user") String user, UpdateUserRequest 
request) {
     return IdpRESTUtils.doAs(
         httpRequest,
         () -> {
           request.validate();
-          userGroupManager.changePassword(user, request.getPassword());
+          if (request.getPassword() != null) {
+            userGroupManager.changePassword(user, request.getPassword());
+          }
+          if (request.getEnabled() != null) {
+            userGroupManager.updateEnabled(user, request.getEnabled());
+          }
           return IdpRESTUtils.ok(new 
IdpUserResponse(userGroupManager.getUser(user).toDTO()));
         },
         "user",
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
index 03547c43b3..5a1fae591f 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
@@ -41,6 +41,7 @@ import org.apache.gravitino.Config;
 import org.apache.gravitino.auth.AuthenticatorType;
 import org.apache.gravitino.exceptions.AlreadyExistsException;
 import org.apache.gravitino.exceptions.NotFoundException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
 import org.apache.gravitino.idp.basic.IdpCredentialValidator;
 import org.apache.gravitino.idp.model.IdpGroup;
 import org.apache.gravitino.idp.model.IdpUser;
@@ -96,8 +97,13 @@ public class TestIdpUserGroupManager {
   public void testAddUser() throws IOException {
     IdpUser user = manager.addUser("testAdd", VALID_PASSWORD);
     Assertions.assertEquals("testAdd", user.name());
+    Assertions.assertTrue(user.enabled());
     Assertions.assertTrue(user.groupNames().isEmpty());
 
+    IdpUser disabled = manager.addUser("testAddDisabled", VALID_PASSWORD, 
false);
+    Assertions.assertFalse(disabled.enabled());
+    Assertions.assertFalse(manager.getUser("testAddDisabled").enabled());
+
     Assertions.assertThrows(
         AlreadyExistsException.class, () -> manager.addUser("testAdd", 
ANOTHER_VALID_PASSWORD));
   }
@@ -133,6 +139,52 @@ public class TestIdpUserGroupManager {
         NotFoundException.class, () -> manager.changePassword("not-exist", 
VALID_PASSWORD));
   }
 
+  @Test
+  public void testUpdateEnabled() throws IOException {
+    manager.addUser("testEnabled", VALID_PASSWORD);
+    Assertions.assertTrue(manager.getUser("testEnabled").enabled());
+    Assertions.assertEquals(
+        "testEnabled", manager.authenticate("testEnabled", 
VALID_PASSWORD).name());
+
+    Assertions.assertTrue(manager.updateEnabled("testEnabled", false));
+    Assertions.assertFalse(manager.getUser("testEnabled").enabled());
+    Assertions.assertThrows(
+        UnauthorizedException.class, () -> manager.authenticate("testEnabled", 
VALID_PASSWORD));
+
+    Assertions.assertTrue(manager.updateEnabled("testEnabled", true));
+    Assertions.assertTrue(manager.getUser("testEnabled").enabled());
+    Assertions.assertEquals(
+        "testEnabled", manager.authenticate("testEnabled", 
VALID_PASSWORD).name());
+
+    Assertions.assertThrows(
+        NotFoundException.class, () -> manager.updateEnabled("not-exist", 
false));
+  }
+
+  @Test
+  public void testCannotDisableServiceAdmin() throws IOException {
+    loadServiceAdminConfig(BASIC_AUTHENTICATOR, "svcAdminDisable");
+    manager.addUser("svcAdminDisable", VALID_PASSWORD);
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class, () -> 
manager.updateEnabled("svcAdminDisable", false));
+    Assertions.assertEquals("Cannot disable service admin svcAdminDisable", 
exception.getMessage());
+    Assertions.assertTrue(manager.getUser("svcAdminDisable").enabled());
+    Assertions.assertTrue(manager.updateEnabled("svcAdminDisable", true));
+  }
+
+  @Test
+  public void testCannotAddDisabledServiceAdmin() {
+    loadServiceAdminConfig(BASIC_AUTHENTICATOR, "svcAdminCreateDisabled");
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () -> manager.addUser("svcAdminCreateDisabled", VALID_PASSWORD, 
false));
+    Assertions.assertEquals(
+        "Cannot disable service admin svcAdminCreateDisabled", 
exception.getMessage());
+  }
+
   @Test
   public void testAddGroup() throws IOException {
     IdpGroup group = manager.addGroup("testAddGroup");
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpUserDTO.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpUserDTO.java
index b0cb2545bc..291f1ece36 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpUserDTO.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpUserDTO.java
@@ -52,14 +52,16 @@ public class TestIdpUserDTO {
     Assertions.assertEquals(userDTO1, deserialized1);
     Assertions.assertEquals("test_user", deserialized1.name());
     Assertions.assertTrue(deserialized1.groups().isEmpty());
-    Assertions.assertEquals("IdpUserDTO(name=test_user, groups=[])", 
deserialized1.toString());
+    Assertions.assertEquals(
+        "IdpUserDTO(name=test_user, enabled=true, groups=[])", 
deserialized1.toString());
 
     IdpUserDTO deserializedWithNullGroups =
         JsonUtils.objectMapper()
             .readValue("{\"name\":\"test_user\",\"groups\":null}", 
IdpUserDTO.class);
     Assertions.assertTrue(deserializedWithNullGroups.groups().isEmpty());
     Assertions.assertEquals(
-        "IdpUserDTO(name=test_user, groups=[])", 
deserializedWithNullGroups.toString());
+        "IdpUserDTO(name=test_user, enabled=true, groups=[])",
+        deserializedWithNullGroups.toString());
 
     Assertions.assertThrows(
         IllegalArgumentException.class, () -> IdpUserDTO.builder().withName(" 
").build());
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/requests/TestAddUserRequest.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/requests/TestAddUserRequest.java
index 3754a387b3..4f4052e258 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/requests/TestAddUserRequest.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/requests/TestAddUserRequest.java
@@ -49,6 +49,8 @@ public class TestAddUserRequest {
     Assertions.assertEquals(request1, deserRequest1);
     Assertions.assertNull(deserRequest1.getUser());
     Assertions.assertNull(deserRequest1.getPassword());
+    Assertions.assertNull(deserRequest1.getEnabled());
+    Assertions.assertTrue(deserRequest1.enabledOrDefault());
   }
 
   @Test
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/requests/TestChangePasswordRequest.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/requests/TestUpdateUserRequest.java
similarity index 51%
rename from 
plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/requests/TestChangePasswordRequest.java
rename to 
plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/requests/TestUpdateUserRequest.java
index 1a6b57a0fb..9fa7c88a0c 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/requests/TestChangePasswordRequest.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/requests/TestUpdateUserRequest.java
@@ -7,7 +7,7 @@
  * "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
+ * 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
@@ -24,46 +24,57 @@ import org.apache.gravitino.json.JsonUtils;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
-public class TestChangePasswordRequest {
+public class TestUpdateUserRequest {
 
   private static final String VALID_PASSWORD = "new_password12";
 
   @Test
-  public void testChangePasswordRequestSerDe() throws JsonProcessingException {
-    ChangePasswordRequest request = new ChangePasswordRequest(VALID_PASSWORD);
+  public void testUpdateUserRequestSerDe() throws JsonProcessingException {
+    UpdateUserRequest request = new UpdateUserRequest(VALID_PASSWORD);
 
     String serJson = JsonUtils.objectMapper().writeValueAsString(request);
-    ChangePasswordRequest deserRequest =
-        JsonUtils.objectMapper().readValue(serJson, 
ChangePasswordRequest.class);
+    UpdateUserRequest deserRequest =
+        JsonUtils.objectMapper().readValue(serJson, UpdateUserRequest.class);
 
     Assertions.assertEquals(request, deserRequest);
     Assertions.assertEquals(VALID_PASSWORD, deserRequest.getPassword());
 
     // Test with null password
-    ChangePasswordRequest request1 = new ChangePasswordRequest();
+    UpdateUserRequest request1 = new UpdateUserRequest();
 
     String serJson1 = JsonUtils.objectMapper().writeValueAsString(request1);
-    ChangePasswordRequest deserRequest1 =
-        JsonUtils.objectMapper().readValue(serJson1, 
ChangePasswordRequest.class);
+    UpdateUserRequest deserRequest1 =
+        JsonUtils.objectMapper().readValue(serJson1, UpdateUserRequest.class);
 
     Assertions.assertEquals(request1, deserRequest1);
     Assertions.assertNull(deserRequest1.getPassword());
+
+    UpdateUserRequest enabledOnly = new UpdateUserRequest(null, false);
+    UpdateUserRequest deserEnabled =
+        JsonUtils.objectMapper()
+            .readValue(
+                JsonUtils.objectMapper().writeValueAsString(enabledOnly), 
UpdateUserRequest.class);
+    Assertions.assertEquals(enabledOnly, deserEnabled);
+    Assertions.assertNull(deserEnabled.getPassword());
+    Assertions.assertFalse(deserEnabled.getEnabled());
   }
 
   @Test
-  public void testChangePasswordRequestValidate() {
-    Assertions.assertDoesNotThrow(() -> new 
ChangePasswordRequest(VALID_PASSWORD).validate());
+  public void testUpdateUserRequestValidate() {
+    Assertions.assertDoesNotThrow(() -> new 
UpdateUserRequest(VALID_PASSWORD).validate());
+    Assertions.assertDoesNotThrow(() -> new UpdateUserRequest(null, 
false).validate());
+    Assertions.assertDoesNotThrow(() -> new UpdateUserRequest(VALID_PASSWORD, 
true).validate());
     Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new 
ChangePasswordRequest().validate());
+        IllegalArgumentException.class, () -> new 
UpdateUserRequest().validate());
     Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new ChangePasswordRequest(" 
").validate());
+        IllegalArgumentException.class, () -> new UpdateUserRequest(" 
").validate());
     Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new 
ChangePasswordRequest("short").validate());
+        IllegalArgumentException.class, () -> new 
UpdateUserRequest("short").validate());
   }
 
   @Test
-  public void testChangePasswordRequestToStringDoesNotExposePassword() {
-    String requestString = new 
ChangePasswordRequest(VALID_PASSWORD).toString();
+  public void testUpdateUserRequestToStringDoesNotExposePassword() {
+    String requestString = new UpdateUserRequest(VALID_PASSWORD).toString();
 
     Assertions.assertFalse(requestString.contains(VALID_PASSWORD));
     Assertions.assertFalse(requestString.contains("password="));
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
index 78c13129ca..026dfca21d 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
@@ -42,8 +42,8 @@ import org.apache.gravitino.auth.AuthenticatorType;
 import org.apache.gravitino.dto.responses.ErrorConstants;
 import org.apache.gravitino.idp.dto.requests.AddGroupRequest;
 import org.apache.gravitino.idp.dto.requests.AddUserRequest;
-import org.apache.gravitino.idp.dto.requests.ChangePasswordRequest;
 import org.apache.gravitino.idp.dto.requests.GroupMembershipChangeRequest;
+import org.apache.gravitino.idp.dto.requests.UpdateUserRequest;
 import org.apache.gravitino.idp.dto.responses.IdpGroupResponse;
 import org.apache.gravitino.idp.dto.responses.IdpUserResponse;
 import org.apache.gravitino.idp.web.rest.feature.IdpRESTFeature;
@@ -165,17 +165,27 @@ public class IdpRESTApiIT extends BaseIT {
     changePassword(USER1, UPDATED_PASSWORD);
     Assertions.assertEquals(USER1, getUser(USER1).getUser().name());
 
+    put("/idp/users/" + USER1, new UpdateUserRequest(null, false));
+    Assertions.assertFalse(getUser(USER1).getUser().enabled());
+    put("/idp/users/" + USER1, new UpdateUserRequest(null, true));
+    Assertions.assertTrue(getUser(USER1).getUser().enabled());
+    assertError(
+        400,
+        put("/idp/users/" + ADMIN, new UpdateUserRequest(null, false)),
+        ErrorConstants.ILLEGAL_ARGUMENTS_CODE);
+    Assertions.assertTrue(getUser(ADMIN).getUser().enabled());
+
     assertError(
         404,
         get("/idp/users/" + MISSING_USER, ADMIN, ADMIN_PASSWORD),
         ErrorConstants.NOT_FOUND_CODE);
     assertError(
         404,
-        put("/idp/users/" + MISSING_USER, new 
ChangePasswordRequest(UPDATED_PASSWORD)),
+        put("/idp/users/" + MISSING_USER, new 
UpdateUserRequest(UPDATED_PASSWORD)),
         ErrorConstants.NOT_FOUND_CODE);
     assertError(
         400,
-        put("/idp/users/" + USER1, new ChangePasswordRequest(" ")),
+        put("/idp/users/" + USER1, new UpdateUserRequest(" ")),
         ErrorConstants.ILLEGAL_ARGUMENTS_CODE);
 
     Assertions.assertTrue(deleteUser(USER1));
@@ -293,8 +303,7 @@ public class IdpRESTApiIT extends BaseIT {
   }
 
   private static void changePassword(String username, String password) throws 
Exception {
-    HttpResponse<String> response =
-        put("/idp/users/" + username, new ChangePasswordRequest(password));
+    HttpResponse<String> response = put("/idp/users/" + username, new 
UpdateUserRequest(password));
     Assertions.assertEquals(200, response.statusCode(), response.body());
   }
 
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
index 51b62170aa..e4f00ce74a 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
@@ -38,6 +38,7 @@ public class TestIdpUserPO {
     Assertions.assertEquals(1L, userPO.getUserId());
     Assertions.assertEquals("alice", userPO.getUsername());
     Assertions.assertEquals("hash", userPO.getPasswordHash());
+    Assertions.assertTrue(userPO.getEnabled());
     Assertions.assertEquals(1L, userPO.getCurrentVersion());
     Assertions.assertEquals(1L, userPO.getLastVersion());
     Assertions.assertEquals(0L, userPO.getDeletedAt());
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
index e732bb0f92..fc1092f296 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
@@ -19,6 +19,7 @@
 package org.apache.gravitino.idp.storage.service;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertIterableEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -105,6 +106,24 @@ class TestIdpUserMetaService extends 
AbstractIdpMetaServiceTest {
     runServiceCall(() -> 
assertTrue(userMetaService.updateIdpUserPassword("user1", "hash-2")));
   }
 
+  @ParameterizedTest
+  @MethodSource("storageProvider")
+  void testUpdateIdpUserEnabled(String type) throws IOException {
+    init(type);
+    insertUsers(1);
+    IdpUserMetaService userMetaService = IdpUserMetaService.getInstance();
+
+    assertTrue(userMetaService.getIdpUserByUsername("user1").getEnabled());
+    closeSession();
+    assertThrows(
+        NotFoundException.class, () -> 
userMetaService.updateIdpUserEnabled("missing", false));
+    refreshSession();
+
+    runServiceCall(() -> 
assertTrue(userMetaService.updateIdpUserEnabled("user1", false)));
+    assertEquals(false, 
userMetaService.getIdpUserByUsername("user1").getEnabled());
+    assertFalse(userMetaService.getIdpUser("user1").enabled());
+  }
+
   @ParameterizedTest
   @MethodSource("storageProvider")
   void testDeleteIdpUser(String type) throws IOException {
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpOperations.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpOperations.java
index e165202948..0bc2aa96b2 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpOperations.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpOperations.java
@@ -45,8 +45,8 @@ import org.apache.gravitino.idp.IdpUserGroupManager;
 import org.apache.gravitino.idp.dto.IdpGroupDTO;
 import org.apache.gravitino.idp.dto.requests.AddGroupRequest;
 import org.apache.gravitino.idp.dto.requests.AddUserRequest;
-import org.apache.gravitino.idp.dto.requests.ChangePasswordRequest;
 import org.apache.gravitino.idp.dto.requests.GroupMembershipChangeRequest;
+import org.apache.gravitino.idp.dto.requests.UpdateUserRequest;
 import org.apache.gravitino.idp.dto.responses.IdpGroupResponse;
 import org.apache.gravitino.idp.dto.responses.IdpUserResponse;
 import org.apache.gravitino.idp.model.IdpGroup;
@@ -101,7 +101,7 @@ class TestIdpOperations extends JerseyTest {
   @Test
   void testAddUser() throws Exception {
     AddUserRequest req = new AddUserRequest("user1", VALID_PASSWORD);
-    doReturn(buildUser("user1")).when(MANAGER).addUser("user1", 
VALID_PASSWORD);
+    doReturn(buildUser("user1")).when(MANAGER).addUser("user1", 
VALID_PASSWORD, true);
 
     assertError(
         Response.Status.BAD_REQUEST,
@@ -113,7 +113,7 @@ class TestIdpOperations extends JerseyTest {
 
     doThrow(new AlreadyExistsException("mock error"))
         .when(MANAGER)
-        .addUser("user1", VALID_PASSWORD);
+        .addUser("user1", VALID_PASSWORD, true);
     assertStatus(Response.Status.CONFLICT, post("/idp/users", req));
   }
 
@@ -130,7 +130,7 @@ class TestIdpOperations extends JerseyTest {
 
   @Test
   void testChangePasswordAndRemoveUser() {
-    ChangePasswordRequest req = new ChangePasswordRequest(VALID_PASSWORD);
+    UpdateUserRequest req = new UpdateUserRequest(VALID_PASSWORD);
     when(MANAGER.changePassword("user1", VALID_PASSWORD)).thenReturn(true);
     when(MANAGER.getUser("user1")).thenReturn(buildUser("user1"));
     when(MANAGER.removeUser("user1")).thenReturn(true);
@@ -140,6 +140,29 @@ class TestIdpOperations extends JerseyTest {
     
Assertions.assertTrue(delete("/idp/users/user1").readEntity(RemoveResponse.class).removed());
   }
 
+  @Test
+  void testUpdateEnabled() {
+    UpdateUserRequest req = new UpdateUserRequest(null, false);
+    when(MANAGER.updateEnabled("user1", false)).thenReturn(true);
+    when(MANAGER.getUser("user1")).thenReturn(new IdpUser("user1", 
Collections.emptyList(), false));
+
+    Assertions.assertFalse(
+        put("/idp/users/user1", 
req).readEntity(IdpUserResponse.class).getUser().enabled());
+  }
+
+  @Test
+  void testCannotDisableServiceAdmin() {
+    UpdateUserRequest req = new UpdateUserRequest(null, false);
+    doThrow(new IllegalArgumentException("Cannot disable service admin admin"))
+        .when(MANAGER)
+        .updateEnabled("admin", false);
+
+    assertError(
+        Response.Status.BAD_REQUEST,
+        put("/idp/users/admin", req),
+        ErrorConstants.ILLEGAL_ARGUMENTS_CODE);
+  }
+
   @Test
   void testAddAndGetGroup() throws Exception {
     AddGroupRequest req = new AddGroupRequest("group1");
diff --git a/scripts/h2/schema-2.0.0-h2.sql b/scripts/h2/schema-2.0.0-h2.sql
index d5a4e5baf6..9b185bcc0b 100644
--- a/scripts/h2/schema-2.0.0-h2.sql
+++ b/scripts/h2/schema-2.0.0-h2.sql
@@ -259,6 +259,7 @@ CREATE TABLE IF NOT EXISTS `idp_user_meta` (
     `user_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'idp user id',
     `user_name` VARCHAR(128) NOT NULL COMMENT 'idp username',
     `password_hash` VARCHAR(1024) NOT NULL COMMENT 'idp user password hash',
+    `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'whether the user is 
enabled, 0 is disabled, 1 is enabled',
     `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp user 
current version',
     `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp user last 
version',
     `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'idp user 
deleted at',
diff --git a/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql 
b/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql
index e3238121b7..5ba0d646b8 100644
--- a/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql
+++ b/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql
@@ -37,6 +37,8 @@ ALTER TABLE `tag_relation_meta` DROP INDEX `uk_ti_mi_del`;
 
 ALTER TABLE `tag_relation_meta` ADD COLUMN `tag_value` VARCHAR(256) NOT NULL 
DEFAULT '' COMMENT 'tag assignment value, empty string means no value' AFTER 
`metadata_object_type`;
 
+ALTER TABLE `idp_user_meta` ADD COLUMN `enabled` TINYINT(1) NOT NULL DEFAULT 1 
COMMENT 'whether the user is enabled, 0 is disabled, 1 is enabled' AFTER 
`password_hash`;
+
 ALTER TABLE `idp_group_meta` ADD COLUMN `group_comment` VARCHAR(1024) DEFAULT 
'' COMMENT 'idp group comment' AFTER `group_name`;
 
 CREATE UNIQUE INDEX IF NOT EXISTS `uk_ti_mi_mo_tv_del` ON `tag_relation_meta` 
(`tag_id`, `metadata_object_id`, `metadata_object_type`, `tag_value`, 
`deleted_at`);
diff --git a/scripts/mysql/schema-2.0.0-mysql.sql 
b/scripts/mysql/schema-2.0.0-mysql.sql
index c51e99f937..a6f64a62d8 100644
--- a/scripts/mysql/schema-2.0.0-mysql.sql
+++ b/scripts/mysql/schema-2.0.0-mysql.sql
@@ -250,6 +250,7 @@ CREATE TABLE IF NOT EXISTS `idp_user_meta` (
     `user_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'idp user id',
     `user_name` VARCHAR(128) NOT NULL COMMENT 'idp username',
     `password_hash` VARCHAR(1024) NOT NULL COMMENT 'idp user password hash',
+    `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'whether the user is 
enabled, 0 is disabled, 1 is enabled',
     `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp user 
current version',
     `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp user last 
version',
     `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'idp user 
deleted at',
diff --git a/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql 
b/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql
index 7342a0905d..aa7203f5ef 100644
--- a/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql
+++ b/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql
@@ -39,6 +39,9 @@ ALTER TABLE `tag_relation_meta`
 ALTER TABLE `tag_relation_meta`
     ADD COLUMN `tag_value` VARCHAR(256) NOT NULL DEFAULT '' COMMENT 'tag 
assignment value, empty string means no value' AFTER `metadata_object_type`;
 
+ALTER TABLE `idp_user_meta`
+    ADD COLUMN `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'whether the 
user is enabled, 0 is disabled, 1 is enabled' AFTER `password_hash`;
+
 ALTER TABLE `idp_group_meta`
     ADD COLUMN `group_comment` VARCHAR(1024) DEFAULT '' COMMENT 'idp group 
comment' AFTER `group_name`;
 
diff --git a/scripts/postgresql/schema-2.0.0-postgresql.sql 
b/scripts/postgresql/schema-2.0.0-postgresql.sql
index 9c8da3beee..2fdcb694f3 100644
--- a/scripts/postgresql/schema-2.0.0-postgresql.sql
+++ b/scripts/postgresql/schema-2.0.0-postgresql.sql
@@ -441,6 +441,7 @@ CREATE TABLE IF NOT EXISTS idp_user_meta (
     user_id BIGINT NOT NULL,
     user_name VARCHAR(128) NOT NULL,
     password_hash VARCHAR(1024) NOT NULL,
+    enabled BOOLEAN NOT NULL DEFAULT TRUE,
     current_version INT NOT NULL DEFAULT 1,
     last_version INT NOT NULL DEFAULT 1,
     deleted_at BIGINT NOT NULL DEFAULT 0,
@@ -452,6 +453,7 @@ COMMENT ON TABLE idp_user_meta IS 'local IdP user metadata';
 COMMENT ON COLUMN idp_user_meta.user_id IS 'idp user id';
 COMMENT ON COLUMN idp_user_meta.user_name IS 'idp username';
 COMMENT ON COLUMN idp_user_meta.password_hash IS 'idp user password hash';
+COMMENT ON COLUMN idp_user_meta.enabled IS 'whether the user is enabled, 0 is 
disabled, 1 is enabled';
 COMMENT ON COLUMN idp_user_meta.current_version IS 'idp user current version';
 COMMENT ON COLUMN idp_user_meta.last_version IS 'idp user last version';
 COMMENT ON COLUMN idp_user_meta.deleted_at IS 'idp user deleted at';
diff --git a/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql 
b/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql
index 79c1a4f032..77e3b79c02 100644
--- a/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql
+++ b/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql
@@ -43,6 +43,9 @@ COMMENT ON COLUMN tag_meta.allowed_values IS 'tag allowed 
values as a JSON strin
 ALTER TABLE tag_relation_meta ADD COLUMN IF NOT EXISTS tag_value VARCHAR(256) 
NOT NULL DEFAULT '';
 COMMENT ON COLUMN tag_relation_meta.tag_value IS 'tag assignment value, empty 
string means no value';
 
+ALTER TABLE idp_user_meta ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL 
DEFAULT TRUE;
+COMMENT ON COLUMN idp_user_meta.enabled IS 'whether the user is enabled, 0 is 
disabled, 1 is enabled';
+
 ALTER TABLE idp_group_meta ADD COLUMN IF NOT EXISTS group_comment 
VARCHAR(1024) DEFAULT '';
 COMMENT ON COLUMN idp_group_meta.group_comment IS 'idp group comment';
 

Reply via email to