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';