You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ozone.apache.org by si...@apache.org on 2021/10/28 22:13:46 UTC

[ozone] branch HDDS-4944 updated: HDDS-5776. [Multi-Tenant] Implement AssignTenantAdmin, RevokeTenantAdmin, ListTenant, RevokeAccessID (#2734)

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

siyao pushed a commit to branch HDDS-4944
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-4944 by this push:
     new 8ecff6f  HDDS-5776. [Multi-Tenant] Implement AssignTenantAdmin, RevokeTenantAdmin, ListTenant, RevokeAccessID (#2734)
8ecff6f is described below

commit 8ecff6fd1a5842e8c81c125fd1d5aa729b7c7a47
Author: Siyao Meng <50...@users.noreply.github.com>
AuthorDate: Thu Oct 28 15:13:24 2021 -0700

    HDDS-5776. [Multi-Tenant] Implement AssignTenantAdmin, RevokeTenantAdmin, ListTenant, RevokeAccessID (#2734)
---
 .../java/org/apache/hadoop/ozone/OzoneConsts.java  |  17 +-
 .../apache/hadoop/ozone/client/ObjectStore.java    |  53 ++++-
 .../ozone/client/protocol/ClientProtocol.java      |  35 ++-
 .../apache/hadoop/ozone/client/rpc/RpcClient.java  |  61 ++++-
 .../main/java/org/apache/hadoop/ozone/OmUtils.java |   7 +-
 .../org/apache/hadoop/ozone/audit/OMAction.java    |   8 +-
 .../hadoop/ozone/om/exceptions/OMException.java    |   7 +-
 .../hadoop/ozone/om/helpers/OmDBAccessIdInfo.java  |  60 ++++-
 .../om/helpers/OmDBKerberosPrincipalInfo.java      |   6 +-
 .../hadoop/ozone/om/helpers/TenantInfoList.java    |  71 ++++++
 .../hadoop/ozone/om/lock/OzoneManagerLock.java     |   1 +
 .../om/multitenant/CephCompatibleTenantImpl.java   |  18 +-
 .../multitenant/MultiTenantAccessAuthorizer.java   |  35 +--
 .../MultiTenantAccessAuthorizerDummyPlugin.java    |  16 +-
 .../MultiTenantAccessAuthorizerRangerPlugin.java   | 196 ++++++++--------
 .../ozone/om/multitenant/OzoneOwnerPrincipal.java} |  24 +-
 ...rincipal.java => OzoneTenantRolePrincipal.java} |  20 +-
 .../ozone/om/multitenant/RangerAccessPolicy.java   |   4 +-
 .../apache/hadoop/ozone/om/multitenant/Tenant.java |   6 +-
 .../ozone/om/protocol/OzoneManagerProtocol.java    |  35 ++-
 ...OzoneManagerProtocolClientSideTranslatorPB.java | 114 +++++++++-
 .../ozonesecure/mockserverInitialization.json      |  20 +-
 .../smoketest/security/ozone-secure-tenant.robot   |  17 +-
 ...estMultiTenantAccessAuthorizerRangerPlugin.java |  63 +++---
 .../om/multitenant/TestMultiTenantVolume.java      |   2 +-
 .../hadoop/ozone/shell/TestOzoneTenantShell.java   | 213 ++++++++++++++---
 .../src/main/proto/OmClientProtocol.proto          | 115 +++++++---
 .../apache/hadoop/ozone/om/OMMetadataManager.java  |   2 -
 .../hadoop/ozone/om/OMMultiTenantManager.java      |   9 +-
 .../hadoop/ozone/om/OMMultiTenantManagerImpl.java  | 186 +++++++++------
 .../hadoop/ozone/om/OmMetadataManagerImpl.java     |  19 +-
 .../org/apache/hadoop/ozone/om/OzoneManager.java   | 150 +++++++++++-
 .../hadoop/ozone/om/codec/OMDBDefinition.java      |  14 +-
 .../om/ratis/utils/OzoneManagerRatisUtils.java     |  16 +-
 .../s3/tenant/OMAssignUserToTenantRequest.java     | 174 +++++++++-----
 .../tenant/OMRevokeUserAccessToTenantRequest.java  |  50 ----
 .../s3/tenant/OMTenantAssignAdminRequest.java      | 246 ++++++++++++++++++++
 .../request/s3/tenant/OMTenantCreateRequest.java   |  14 +-
 .../request/s3/tenant/OMTenantRequestHelper.java   | 143 ++++++++++++
 .../s3/tenant/OMTenantRevokeAdminRequest.java      | 221 ++++++++++++++++++
 .../tenant/OMTenantRevokeUserAccessIdRequest.java  | 251 +++++++++++++++++++++
 .../s3/tenant/OMTenantAssignAdminResponse.java     |  76 +++++++
 ...ava => OMTenantAssignUserAccessIdResponse.java} |  10 +-
 .../s3/tenant/OMTenantRevokeAdminResponse.java     |  76 +++++++
 ...ava => OMTenantRevokeUserAccessIdResponse.java} |  62 ++---
 .../protocolPB/OzoneManagerRequestHandler.java     |  21 ++
 .../s3/security/TestS3GetSecretRequest.java        |  26 ++-
 .../ozone/shell/tenant/GetUserInfoHandler.java     |  14 +-
 .../shell/tenant/TenantAssignAdminHandler.java     |  80 +++++++
 ...r.java => TenantAssignUserAccessIdHandler.java} |  36 +--
 .../ozone/shell/tenant/TenantListHandler.java      |  90 ++++++++
 .../shell/tenant/TenantRevokeAdminHandler.java     |  74 ++++++
 ...r.java => TenantRevokeUserAccessIdHandler.java} |  27 ++-
 .../hadoop/ozone/shell/tenant/TenantShell.java     |   1 +
 .../ozone/shell/tenant/TenantUserCommands.java     |   6 +-
 55 files changed, 2683 insertions(+), 635 deletions(-)

diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
index 371885a..9eb96db 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
@@ -337,6 +337,7 @@ public final class OzoneConsts {
 
   // For multi-tenancy
   public static final String TENANT_NAME_USER_NAME_DELIMITER = "$";
+  public static final String TENANT_NAME_ROLE_DELIMITER = "-";
   public static final String DEFAULT_TENANT_USER_POLICY_SUFFIX = "-users";
   public static final String DEFAULT_TENANT_BUCKET_POLICY_SUFFIX = "-buckets";
   public static final String DEFAULT_TENANT_POLICY_ID_SUFFIX = "-default";
@@ -458,18 +459,26 @@ public final class OzoneConsts {
   public static final String OZONE_OM_RANGER_ADMIN_CREATE_USER_HTTP_ENDPOINT =
       "/service/xusers/secure/users";
 
+  // Ideally we should use /addUsersAndGroups endpoint for add user to role,
+  // but it always return 405 somehow.
+  // https://ranger.apache.org/apidocs/resource_RoleREST.html
+  // #resource_RoleREST_addUsersAndGroups_PUT
+  public static final String OZONE_OM_RANGER_ADMIN_ROLE_ADD_USER_HTTP_ENDPOINT =
+      "/service/roles/roles/";
+
   public static final String OZONE_OM_RANGER_ADMIN_GET_USER_HTTP_ENDPOINT =
       "/service/xusers/users/?name=";
 
   public static final String OZONE_OM_RANGER_ADMIN_DELETE_USER_HTTP_ENDPOINT =
       "/service/xusers/secure/users/id/";
 
-  public static final String OZONE_OM_RANGER_ADMIN_CREATE_GROUP_HTTP_ENDPOINT =
-      "/service/xusers/secure/groups";
+  public static final String OZONE_OM_RANGER_ADMIN_CREATE_ROLE_HTTP_ENDPOINT =
+      "/service/roles/roles";
 
-  public static final String OZONE_OM_RANGER_ADMIN_GET_GROUP_HTTP_ENDPOINT =
-      "/service/xusers/groups?name=";
+  public static final String OZONE_OM_RANGER_ADMIN_GET_ROLE_HTTP_ENDPOINT =
+      "/service/roles/roles/name/";
 
+  // TODO: Change to delete role endpoint
   public static final String OZONE_OM_RANGER_ADMIN_DELETE_GROUP_HTTP_ENDPOINT =
       "/service/xusers/secure/groups/id/";
 
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
index acc18b7..ba6f353 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
@@ -34,6 +34,7 @@ import org.apache.hadoop.ozone.OzoneAcl;
 import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.helpers.S3SecretValue;
+import org.apache.hadoop.ozone.om.helpers.TenantInfoList;
 import org.apache.hadoop.ozone.om.helpers.TenantUserInfoValue;
 import org.apache.hadoop.ozone.security.OzoneTokenIdentifier;
 import org.apache.hadoop.ozone.security.acl.OzoneObj;
@@ -196,18 +197,49 @@ public class ObjectStore {
 //    proxy.createTenant(tenantName, tenantArgs);
 //  }
 
-  // TODO: modify, delete
-
   /**
-   * Assign user to tenant.
+   * Assign user accessId to tenant.
    * @param username user name to be assigned.
    * @param tenantName tenant name.
-   * @param accessId access ID.
+   * @param accessId Specified accessId.
    * @throws IOException
    */
-  public S3SecretValue assignUserToTenant(
+  // TODO: Rename this to tenantAssignUserAccessId ?
+  public S3SecretValue tenantAssignUserAccessId(
       String username, String tenantName, String accessId) throws IOException {
-    return proxy.assignUserToTenant(username, tenantName, accessId);
+    return proxy.tenantAssignUserAccessId(username, tenantName, accessId);
+  }
+
+  /**
+   * Revoke user accessId to tenant.
+   * @param accessId accessId to be revoked.
+   * @throws IOException
+   */
+  public void tenantRevokeUserAccessId(String accessId) throws IOException {
+    proxy.tenantRevokeUserAccessId(accessId);
+  }
+
+  /**
+   * Assign admin role to an accessId in a tenant.
+   * @param accessId access ID.
+   * @param tenantName tenant name.
+   * @param delegated true if making delegated admin.
+   * @throws IOException
+   */
+  public void tenantAssignAdmin(String accessId, String tenantName,
+      boolean delegated) throws IOException {
+    proxy.tenantAssignAdmin(accessId, tenantName, delegated);
+  }
+
+  /**
+   * Revoke admin role of an accessId from a tenant.
+   * @param accessId access ID.
+   * @param tenantName tenant name.
+   * @throws IOException
+   */
+  public void tenantRevokeAdmin(String accessId, String tenantName)
+      throws IOException {
+    proxy.tenantRevokeAdmin(accessId, tenantName);
   }
 
   /**
@@ -222,6 +254,15 @@ public class ObjectStore {
   }
 
   /**
+   * List tenants.
+   * @return TenantInfoList
+   * @throws IOException
+   */
+  public TenantInfoList listTenant() throws IOException {
+    return proxy.listTenant();
+  }
+
+  /**
    * Returns Iterator to iterate over all the volumes in object store.
    * The result can be restricted using volume prefix, will return all
    * volumes if volume prefix is null.
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index 69938aa..3845250 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -47,6 +47,7 @@ import org.apache.hadoop.ozone.om.helpers.OmMultipartUploadCompleteInfo;
 import org.apache.hadoop.ozone.om.helpers.OzoneFileStatus;
 import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.S3SecretValue;
+import org.apache.hadoop.ozone.om.helpers.TenantInfoList;
 import org.apache.hadoop.ozone.om.helpers.TenantUserInfoValue;
 import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRoleInfo;
@@ -588,10 +589,35 @@ public interface ClientProtocol {
    * @param accessId access ID.
    * @throws IOException
    */
-  S3SecretValue assignUserToTenant(String username, String tenantName,
+  S3SecretValue tenantAssignUserAccessId(String username, String tenantName,
       String accessId) throws IOException;
 
   /**
+   * Revoke user accessId to tenant.
+   * @param accessId accessId to be revoked.
+   * @throws IOException
+   */
+  void tenantRevokeUserAccessId(String accessId) throws IOException;
+
+  /**
+   * Assign admin role to an accessId in a tenant.
+   * @param accessId access ID.
+   * @param tenantName tenant name.
+   * @param delegated true if making delegated admin.
+   * @throws IOException
+   */
+  void tenantAssignAdmin(String accessId, String tenantName,
+      boolean delegated) throws IOException;
+
+  /**
+   * Revoke admin role of an accessId from a tenant.
+   * @param accessId access ID.
+   * @param tenantName tenant name.
+   * @throws IOException
+   */
+  void tenantRevokeAdmin(String accessId, String tenantName) throws IOException;
+
+  /**
    * Get tenant info for a user.
    * @param userPrincipal Kerberos principal of a user.
    * @return TenantUserInfo
@@ -601,6 +627,13 @@ public interface ClientProtocol {
       throws IOException;
 
   /**
+   * List tenants.
+   * @return TenantInfoList
+   * @throws IOException
+   */
+  TenantInfoList listTenant() throws IOException;
+
+  /**
    * Get KMS client provider.
    * @return KMS client provider.
    * @throws IOException
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 146db37..eabbfbb 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -104,6 +104,7 @@ import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.S3SecretValue;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
+import org.apache.hadoop.ozone.om.helpers.TenantInfoList;
 import org.apache.hadoop.ozone.om.helpers.TenantUserInfoValue;
 import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
 import org.apache.hadoop.ozone.om.protocolPB.OmTransport;
@@ -645,8 +646,6 @@ public class RpcClient implements ClientProtocol {
     ozoneManagerClient.createTenant(tenantName);
   }
 
-  // TODO: modify, delete
-
   /**
    * Assign user to tenant.
    * @param username user name to be assigned.
@@ -654,7 +653,7 @@ public class RpcClient implements ClientProtocol {
    * @throws IOException
    */
   @Override
-  public S3SecretValue assignUserToTenant(
+  public S3SecretValue tenantAssignUserAccessId(
       String username, String tenantName, String accessId) throws IOException {
     Preconditions.checkArgument(Strings.isNotBlank(username),
         "username can't be null or empty.");
@@ -662,11 +661,55 @@ public class RpcClient implements ClientProtocol {
         "tenantName can't be null or empty.");
     Preconditions.checkArgument(Strings.isNotBlank(accessId),
         "accessId can't be null or empty.");
-    return ozoneManagerClient.assignUserToTenant(
+    return ozoneManagerClient.tenantAssignUserAccessId(
         username, tenantName, accessId);
   }
 
   /**
+   * Revoke user accessId to tenant.
+   * @param accessId accessId to be revoked.
+   * @throws IOException
+   */
+  @Override
+  public void tenantRevokeUserAccessId(String accessId) throws IOException {
+    Preconditions.checkArgument(Strings.isNotBlank(accessId),
+        "accessId can't be null or empty.");
+    ozoneManagerClient.tenantRevokeUserAccessId(accessId);
+  }
+
+  /**
+   * Assign admin role to an accessId in a tenant.
+   * @param accessId access ID.
+   * @param tenantName tenant name.
+   * @param delegated true if making delegated admin.
+   * @throws IOException
+   */
+  @Override
+  public void tenantAssignAdmin(String accessId, String tenantName,
+      boolean delegated)
+      throws IOException {
+    Preconditions.checkArgument(Strings.isNotBlank(accessId),
+        "accessId can't be null or empty.");
+    // tenantName can be empty
+    ozoneManagerClient.tenantAssignAdmin(accessId, tenantName, delegated);
+  }
+
+  /**
+   * Revoke admin role of an accessId from a tenant.
+   * @param accessId access ID.
+   * @param tenantName tenant name.
+   * @throws IOException
+   */
+  @Override
+  public void tenantRevokeAdmin(String accessId, String tenantName)
+      throws IOException {
+    Preconditions.checkArgument(Strings.isNotBlank(accessId),
+        "accessId can't be null or empty.");
+    // tenantName can be empty
+    ozoneManagerClient.tenantRevokeAdmin(accessId, tenantName);
+  }
+
+  /**
    * Get tenant info for a user.
    * @param userPrincipal Kerberos principal of a user.
    * @return TenantUserInfo
@@ -680,6 +723,16 @@ public class RpcClient implements ClientProtocol {
     return ozoneManagerClient.tenantGetUserInfo(userPrincipal);
   }
 
+  /**
+   * List tenants.
+   * @return TenantInfoList
+   * @throws IOException
+   */
+  @Override
+  public TenantInfoList listTenant() throws IOException {
+    return ozoneManagerClient.listTenant();
+  }
+
   @Override
   public void setBucketVersioning(
       String volumeName, String bucketName, Boolean versioning)
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
index e9255a3..8e80634 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
@@ -266,6 +266,7 @@ public final class OmUtils {
     case FinalizeUpgradeProgress:
     case PrepareStatus:
     case GetS3Volume:
+    case ListTenant:
     case TenantGetUserInfo:
       return true;
     case CreateVolume:
@@ -305,8 +306,10 @@ public final class OmUtils {
     case CreateTenant:
     case ModifyTenant:
     case DeleteTenant:
-    case AssignUserToTenant:
-    case RevokeUserAccessToTenant:
+    case TenantAssignUserAccessId:
+    case TenantRevokeUserAccessId:
+    case TenantAssignAdmin:
+    case TenantRevokeAdmin:
       return false;
     default:
       LOG.error("CmdType {} is not categorized as readOnly or not.", cmdType);
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/audit/OMAction.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/audit/OMAction.java
index 961aacb..5d37413 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/audit/OMAction.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/audit/OMAction.java
@@ -76,10 +76,14 @@ public enum OMAction implements AuditAction {
   CREATE_TENANT,
   MODIFY_TENANT,
   DELETE_TENANT,
+  LIST_TENANT,
 
   TENANT_GET_USER_INFO,
-  ASSIGN_USER_TO_TENANT,
-  REVOKE_USER_ACCESS_TO_TENANT;
+  TENANT_ASSIGN_USER_ACCESSID,
+  TENANT_REVOKE_USER_ACCESSID,
+
+  TENANT_ASSIGN_ADMIN,
+  TENANT_REVOKE_ADMIN;
 
   @Override
   public String getAction() {
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/exceptions/OMException.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/exceptions/OMException.java
index 149aaf0..7b8502e 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/exceptions/OMException.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/exceptions/OMException.java
@@ -248,8 +248,9 @@ public class OMException extends IOException {
     TENANT_ALREADY_EXISTS,
     INVALID_TENANT_NAME,
 
-    TENANT_USER_NOT_FOUND,
-    TENANT_USER_ALREADY_EXISTS,
-    INVALID_TENANT_USER_NAME
+    TENANT_USER_ACCESSID_NOT_FOUND,
+    TENANT_USER_ACCESSID_ALREADY_EXISTS,
+    INVALID_TENANT_USER_NAME,
+    INVALID_ACCESSID
   }
 }
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmDBAccessIdInfo.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmDBAccessIdInfo.java
index a9c5366..69b4404 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmDBAccessIdInfo.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmDBAccessIdInfo.java
@@ -33,28 +33,47 @@ public final class OmDBAccessIdInfo {
    */
   private final String kerberosPrincipal;
   /**
-   * Shared secret of the accessId. TODO: Encryption?
+   * Shared secret of the accessId.
    */
   private final String sharedSecret;
+  /**
+   * Whether this accessId is an administrator of the tenant.
+   */
+  private final boolean isAdmin;
+  /**
+   * Whether this accessId is a delegated admin of the tenant.
+   * Only effective if isAdmin is true.
+   */
+  private final boolean isDelegatedAdmin;
 
   // This implies above String fields should NOT contain the split key.
   public static final String SERIALIZATION_SPLIT_KEY = ";";
 
   public OmDBAccessIdInfo(String tenantId,
-      String kerberosPrincipal, String sharedSecret) {
+      String kerberosPrincipal, String sharedSecret,
+      boolean isAdmin, boolean isDelegatedAdmin) {
     this.tenantId = tenantId;
     this.kerberosPrincipal = kerberosPrincipal;
     this.sharedSecret = sharedSecret;
+    this.isAdmin = isAdmin;
+    this.isDelegatedAdmin = isDelegatedAdmin;
   }
 
   private OmDBAccessIdInfo(String accessIdInfoString) {
     String[] tInfo = accessIdInfoString.split(SERIALIZATION_SPLIT_KEY);
-    Preconditions.checkState(tInfo.length == 3,
+    Preconditions.checkState(tInfo.length == 3 || tInfo.length == 5,
         "Incorrect accessIdInfoString");
 
     tenantId = tInfo[0];
     kerberosPrincipal = tInfo[1];
     sharedSecret = tInfo[2];
+    if (tInfo.length == 5) {
+      isAdmin = Boolean.parseBoolean(tInfo[3]);
+      isDelegatedAdmin = Boolean.parseBoolean(tInfo[4]);
+    } else {
+      isAdmin = false;
+      isDelegatedAdmin = false;
+    }
   }
 
   public String getTenantId() {
@@ -62,10 +81,12 @@ public final class OmDBAccessIdInfo {
   }
 
   private String serialize() {
-    StringBuilder sb = new StringBuilder();
-    sb.append(tenantId).append(SERIALIZATION_SPLIT_KEY);
-    sb.append(kerberosPrincipal).append(SERIALIZATION_SPLIT_KEY);
-    sb.append(sharedSecret);
+    final StringBuilder sb = new StringBuilder();
+    sb.append(tenantId);
+    sb.append(SERIALIZATION_SPLIT_KEY).append(kerberosPrincipal);
+    sb.append(SERIALIZATION_SPLIT_KEY).append(sharedSecret);
+    sb.append(SERIALIZATION_SPLIT_KEY).append(isAdmin);
+    sb.append(SERIALIZATION_SPLIT_KEY).append(isDelegatedAdmin);
     return sb.toString();
   }
 
@@ -93,6 +114,14 @@ public final class OmDBAccessIdInfo {
     return sharedSecret;
   }
 
+  public boolean getIsAdmin() {
+    return isAdmin;
+  }
+
+  public boolean getIsDelegatedAdmin() {
+    return isDelegatedAdmin;
+  }
+
   /**
    * Builder for OmDBAccessIdInfo.
    */
@@ -101,8 +130,10 @@ public final class OmDBAccessIdInfo {
     private String tenantId;
     private String kerberosPrincipal;
     private String sharedSecret;
+    private boolean isAdmin;
+    private boolean isDelegatedAdmin;
 
-    public Builder setTenantName(String tenantId) {
+    public Builder setTenantId(String tenantId) {
       this.tenantId = tenantId;
       return this;
     }
@@ -117,8 +148,19 @@ public final class OmDBAccessIdInfo {
       return this;
     }
 
+    public Builder setIsAdmin(boolean isAdmin) {
+      this.isAdmin = isAdmin;
+      return this;
+    }
+
+    public Builder setIsDelegatedAdmin(boolean isDelegatedAdmin) {
+      this.isDelegatedAdmin = isDelegatedAdmin;
+      return this;
+    }
+
     public OmDBAccessIdInfo build() {
-      return new OmDBAccessIdInfo(tenantId, kerberosPrincipal, sharedSecret);
+      return new OmDBAccessIdInfo(tenantId, kerberosPrincipal, sharedSecret,
+          isAdmin, isDelegatedAdmin);
     }
   }
 }
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmDBKerberosPrincipalInfo.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmDBKerberosPrincipalInfo.java
index a231ad4..c5274fd 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmDBKerberosPrincipalInfo.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmDBKerberosPrincipalInfo.java
@@ -22,6 +22,7 @@ import org.apache.hadoop.hdds.StringUtils;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * This class is used for storing info related to the Kerberos principal.
@@ -45,8 +46,9 @@ public final class OmDBKerberosPrincipalInfo {
   }
 
   private OmDBKerberosPrincipalInfo(String serialized) {
-    accessIds = new HashSet<>(
-        Arrays.asList(serialized.split(SERIALIZATION_SPLIT_KEY)));
+    accessIds = Arrays.stream(serialized.split(SERIALIZATION_SPLIT_KEY))
+        // Remove any empty accessId strings when deserializing
+        .filter(e -> !e.isEmpty()).collect(Collectors.toSet());
   }
 
   public Set<String> getAccessIds() {
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/TenantInfoList.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/TenantInfoList.java
new file mode 100644
index 0000000..c37f989
--- /dev/null
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/TenantInfoList.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.hadoop.ozone.om.helpers;
+
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantInfo;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Utility class to handle protobuf message TenantInfo conversion.
+ */
+public class TenantInfoList {
+
+  // A list of TenantAccessIdInfo from protobuf.
+  private final List<TenantInfo> tenantInfoList;
+
+  public List<TenantInfo> getTenantInfoList() {
+    return tenantInfoList;
+  }
+
+  public TenantInfoList(List<TenantInfo> tenantInfoList) {
+    this.tenantInfoList = tenantInfoList;
+  }
+
+  public static TenantInfoList fromProtobuf(List<TenantInfo> tenantInfoList) {
+    return new TenantInfoList(tenantInfoList);
+  }
+
+  public TenantInfo getProtobuf() {
+    throw new NotImplementedException("getProtobuf() not implemented");
+  }
+
+  @Override
+  public String toString() {
+    return "tenantInfoList=" + tenantInfoList;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    TenantInfoList that = (TenantInfoList) o;
+    return tenantInfoList.equals(that.tenantInfoList);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(tenantInfoList);
+  }
+}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/lock/OzoneManagerLock.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/lock/OzoneManagerLock.java
index ca3de18..2c03573 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/lock/OzoneManagerLock.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/lock/OzoneManagerLock.java
@@ -390,6 +390,7 @@ public class OzoneManagerLock {
 
     S3_SECRET_LOCK((byte) 4, "S3_SECRET_LOCK"), // 31
     PREFIX_LOCK((byte) 5, "PREFIX_LOCK"); //63
+//    TENANT_LOCK((byte) 6, "TENANT_LOCK"); // 127
 
     // level of the resource
     private byte lockLevel;
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/CephCompatibleTenantImpl.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/CephCompatibleTenantImpl.java
index e5b42c9..03d2b6a 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/CephCompatibleTenantImpl.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/CephCompatibleTenantImpl.java
@@ -33,16 +33,15 @@ import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
  */
 public class CephCompatibleTenantImpl implements Tenant {
   private final String tenantID;
-  private List<String> tenantGroupsIDs;
+  private List<String> tenantRoleIds;
   private List<AccessPolicy> accessPolicies;
   private final AccountNameSpace accountNameSpace;
   private final BucketNameSpace bucketNameSpace;
 
-
   public CephCompatibleTenantImpl(String id) {
     tenantID = id;
     accessPolicies = new ArrayList<>();
-    tenantGroupsIDs = new ArrayList<>();
+    tenantRoleIds = new ArrayList<>();
     accountNameSpace = new AccountNameSpaceImpl(id);
     bucketNameSpace = new BucketNameSpaceImpl(id);
     OzoneObj volume = new OzoneObjInfo.Builder()
@@ -83,18 +82,17 @@ public class CephCompatibleTenantImpl implements Tenant {
   }
 
   @Override
-  public void addTenantAccessGroup(String groupID) {
-    tenantGroupsIDs.add(groupID);
-
+  public void addTenantAccessRole(String roleId) {
+    tenantRoleIds.add(roleId);
   }
 
   @Override
-  public void removeTenantAccessGroup(String groupID) {
-    tenantGroupsIDs.remove(groupID);
+  public void removeTenantAccessRole(String roleId) {
+    tenantRoleIds.remove(roleId);
   }
 
   @Override
-  public List<String> getTenantGroups() {
-    return tenantGroupsIDs;
+  public List<String> getTenantRoles() {
+    return tenantRoleIds;
   }
 }
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizer.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizer.java
index 5f0a9fd..72c41ed 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizer.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizer.java
@@ -53,30 +53,32 @@ public interface MultiTenantAccessAuthorizer extends IAccessAuthorizer {
   void shutdown() throws Exception;
 
   /**
-   * Create User Principal entity for MultiTenantGatekeeper plugin.
-   * @param principal
-   * @param groupIDs : groupIDs that this user will belong to
+   * Assign user to an existing role in the Authorizer.
+   * @param principal User principal
+   * @param existingRole A JSON String representation of the existing role
+   *                     returned from the Authorizer (Ranger).
+   * @param isAdmin
    * @return unique and opaque userID that can be used to refer to the user in
    * MultiTenantGateKeeperplugin Implementation. E.g. a Ranger
    * based Implementation can return some ID thats relevant for it.
    */
-  String createUser(BasicUserPrincipal principal,
-                    List<String> groupIDs) throws Exception;
+  String assignUser(BasicUserPrincipal principal, String existingRole,
+      boolean isAdmin) throws IOException;
 
   /**
    * @param principal
    * @return Unique userID maintained by the authorizer plugin.
-   * @throws Exception
+   * @throws IOException
    */
-  String getUserId(BasicUserPrincipal principal) throws Exception;
+  String getUserId(BasicUserPrincipal principal) throws IOException;
 
   /**
    * @param principal
    * @return Unique groupID maintained by the authorizer plugin.
-   * @throws Exception
+   * @throws IOException
    */
-  String getGroupId(OzoneTenantGroupPrincipal principal)
-      throws Exception;
+  String getRole(OzoneTenantRolePrincipal principal)
+      throws IOException;
 
   /**
    * Delete the user userID in MultiTenantGateKeeper plugin.
@@ -87,20 +89,23 @@ public interface MultiTenantAccessAuthorizer extends IAccessAuthorizer {
   void deleteUser(String opaqueUserID) throws IOException;
 
   /**
-   * Create Group group entity for MultiTenantGatekeeper plugin.
-   * @param group
-   * @return unique groupID that can be used to refer to the group in
+   * Create Role entity for MultiTenantGatekeeper plugin.
+   * @param role
+   * @param adminRoleName (Optional) admin role name that will be added to
+   *                      manage this role.
+   * @return unique groupID that can be used to refer to the role in
    * MultiTenantGateKeeper plugin Implementation e.g. corresponding ID on the
    * Ranger end for a ranger based implementation .
    */
-  String createGroup(OzoneTenantGroupPrincipal group) throws Exception;
+  String createRole(OzoneTenantRolePrincipal role, String adminRoleName)
+      throws IOException;
 
   /**
    * Delete the group groupID in MultiTenantGateKeeper plugin.
    * @param groupID : unique opaque ID that was returned by
    *                MultiTenantGatekeeper in createGroup().
    */
-  void deleteGroup(String groupID) throws IOException;
+  void deleteRole(String groupID) throws IOException;
 
   /**
    *
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizerDummyPlugin.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizerDummyPlugin.java
index 3f23a42..582839c 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizerDummyPlugin.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizerDummyPlugin.java
@@ -45,19 +45,18 @@ public class MultiTenantAccessAuthorizerDummyPlugin implements
   }
 
   @Override
-  public String createUser(BasicUserPrincipal principal, List<String> groupIDs)
-      throws Exception {
-    return null;
+  public String assignUser(BasicUserPrincipal principal, String existingRole,
+      boolean isAdmin) throws IOException {
+    return "assignUser-roleId-returned";
   }
 
   @Override
-  public String getUserId(BasicUserPrincipal principal) throws Exception {
+  public String getUserId(BasicUserPrincipal principal) throws IOException {
     return null;
   }
 
   @Override
-  public String getGroupId(OzoneTenantGroupPrincipal principal)
-      throws Exception {
+  public String getRole(OzoneTenantRolePrincipal principal) throws IOException {
     return null;
   }
 
@@ -67,12 +66,13 @@ public class MultiTenantAccessAuthorizerDummyPlugin implements
   }
 
   @Override
-  public String createGroup(OzoneTenantGroupPrincipal group) throws Exception {
+  public String createRole(OzoneTenantRolePrincipal role, String adminRoleName)
+      throws IOException {
     return null;
   }
 
   @Override
-  public void deleteGroup(String groupID) throws IOException {
+  public void deleteRole(String groupID) throws IOException {
 
   }
 
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizerRangerPlugin.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizerRangerPlugin.java
index 94a31dd..b8f5b4e 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizerRangerPlugin.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/MultiTenantAccessAuthorizerRangerPlugin.java
@@ -17,15 +17,16 @@
  */
 package org.apache.hadoop.ozone.om.multitenant;
 
-import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_CREATE_GROUP_HTTP_ENDPOINT;
+import static java.net.HttpURLConnection.HTTP_OK;
 import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_CREATE_POLICY_HTTP_ENDPOINT;
-import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_CREATE_USER_HTTP_ENDPOINT;
+import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_CREATE_ROLE_HTTP_ENDPOINT;
 import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_DELETE_GROUP_HTTP_ENDPOINT;
 import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_DELETE_POLICY_HTTP_ENDPOINT;
 import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_DELETE_USER_HTTP_ENDPOINT;
-import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_GET_GROUP_HTTP_ENDPOINT;
 import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_GET_POLICY_HTTP_ENDPOINT;
+import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_GET_ROLE_HTTP_ENDPOINT;
 import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_GET_USER_HTTP_ENDPOINT;
+import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OM_RANGER_ADMIN_ROLE_ADD_USER_HTTP_ENDPOINT;
 import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RANGER_HTTPS_ADMIN_API_PASSWD;
 import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RANGER_HTTPS_ADMIN_API_USER;
 import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_RANGER_HTTPS_ADDRESS_KEY;
@@ -44,7 +45,6 @@ import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
 
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLContext;
@@ -62,17 +62,16 @@ import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.security.acl.IOzoneObj;
 import org.apache.hadoop.ozone.security.acl.RequestContext;
-import org.apache.hadoop.security.authentication.client.AuthenticationException;
 import org.apache.http.auth.BasicUserPrincipal;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Implements MultiTenantAccessAuthorizer.
+ * Implements MultiTenantAccessAuthorizer for Apache Ranger.
  */
 public class MultiTenantAccessAuthorizerRangerPlugin implements
     MultiTenantAccessAuthorizer {
-  private static final Logger LOG = LoggerFactory
+  public static final Logger LOG = LoggerFactory
       .getLogger(MultiTenantAccessAuthorizerRangerPlugin.class);
 
   private OzoneConfiguration conf;
@@ -207,55 +206,20 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
     // TBD
     return true;
   }
-  private String getCreateUserJsonString(String userName,
-                                         List<String> groupIDs)
-      throws Exception {
-    String groupIdList = groupIDs.stream().collect(Collectors.joining("\",\"",
-        "", ""));
-    String jsonCreateUserString = "{ \"name\":\"" + userName  + "\"," +
-        "\"firstName\":\"" + userName + "\"," +
-        "  \"loginId\": \"" + userName + "\"," +
-        "  \"password\" : \"user1pass\"," +
-        "  \"userRoleList\":[\"ROLE_USER\"]," +
-        "  \"groupIdList\":[\"" + groupIdList +"\"] " +
-        " }";
-    return jsonCreateUserString;
-  }
 
   @Override
-  public String getGroupId(OzoneTenantGroupPrincipal principal)
-      throws Exception {
-    String rangerAdminUrl =
-        rangerHttpsAddress + OZONE_OM_RANGER_ADMIN_GET_GROUP_HTTP_ENDPOINT +
+  public String getRole(OzoneTenantRolePrincipal principal) throws IOException {
+
+    String endpointUrl =
+        rangerHttpsAddress + OZONE_OM_RANGER_ADMIN_GET_ROLE_HTTP_ENDPOINT +
             principal.getName();
 
-    HttpsURLConnection conn = makeHttpsGetCall(rangerAdminUrl,
-        "GET", false);
-    String response = getResponseData(conn);
-    String groupIDCreated = null;
-    try {
-      JsonObject jResonse = new JsonParser().parse(response).getAsJsonObject();
-      JsonArray info = jResonse.get("vXGroups").getAsJsonArray();
-      int numIndex = info.size();
-      for (int i = 0; i < numIndex; ++i) {
-        if (info.get(i).getAsJsonObject().get("name").getAsString()
-            .equals(principal.getName())) {
-          groupIDCreated =
-              info.get(i).getAsJsonObject().get("id").getAsString();
-          break;
-        }
-      }
-      System.out.println("Group ID is : " + groupIDCreated);
-    } catch (JsonParseException e) {
-      e.printStackTrace();
-      throw e;
-    }
-    return groupIDCreated;
+    HttpsURLConnection conn = makeHttpsGetCall(endpointUrl, "GET", false);
+    return getResponseData(conn);
   }
 
   @Override
-  public String getUserId(BasicUserPrincipal principal)
-      throws Exception {
+  public String getUserId(BasicUserPrincipal principal) throws IOException {
     String rangerAdminUrl =
         rangerHttpsAddress + OZONE_OM_RANGER_ADMIN_GET_USER_HTTP_ENDPOINT +
         principal.getName();
@@ -276,7 +240,7 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
           break;
         }
       }
-      System.out.println("User ID is : " + userIDCreated);
+      LOG.debug("User ID is: {}", userIDCreated);
     } catch (JsonParseException e) {
       e.printStackTrace();
       throw e;
@@ -284,65 +248,99 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
     return userIDCreated;
   }
 
-  public String createUser(BasicUserPrincipal principal,
-                           List<String> groupIDs)
-      throws Exception {
-    String rangerAdminUrl =
-        rangerHttpsAddress + OZONE_OM_RANGER_ADMIN_CREATE_USER_HTTP_ENDPOINT;
-
-    String jsonCreateUserString = getCreateUserJsonString(
-        principal.getName(), groupIDs);
-
-    HttpsURLConnection conn = makeHttpsPostCall(rangerAdminUrl,
-        jsonCreateUserString, "POST", false);
-    String userInfo = getResponseData(conn);
-    String userIDCreated;
+  /**
+   * Update the exising role details and push the changes to Ranger.
+   *
+   * @param principal contains user name, must be an existing user in Ranger.
+   * @param existingRole An existing role's JSON response String from Ranger.
+   * @param isAdmin Make it delegated admin of the role.
+   * @return roleId (not useful for now)
+   * @throws IOException
+   */
+  public String assignUser(BasicUserPrincipal principal, String existingRole,
+      boolean isAdmin) throws IOException {
+
+    JsonObject roleObj = new JsonParser().parse(existingRole).getAsJsonObject();
+    // Parse Json
+    final String roleId = roleObj.get("id").getAsString();
+    LOG.debug("Got roleId: {}", roleId);
+
+    JsonArray usersArray = roleObj.getAsJsonArray("users");
+    JsonObject newUserEntry = new JsonObject();
+    newUserEntry.addProperty("name", principal.getName());
+    newUserEntry.addProperty("isAdmin", isAdmin);
+    usersArray.add(newUserEntry);
+    // Update Json array
+    roleObj.add("users", usersArray);
+
+    LOG.debug("Updated: {}", roleObj);
+
+    final String endpointUrl = rangerHttpsAddress +
+        OZONE_OM_RANGER_ADMIN_ROLE_ADD_USER_HTTP_ENDPOINT + roleId;
+    final String jsonData = roleObj.toString();
+
+    HttpsURLConnection conn =
+        makeHttpCall(endpointUrl, jsonData, "PUT", false);
+    if (conn.getResponseCode() != HTTP_OK) {
+      throw new IOException("Ranger REST API failure: " + conn.getResponseCode()
+          + " " + conn.getResponseMessage()
+          + ". Error updating Ranger role.");
+    }
+    String resp = getResponseData(conn);
+    String returnedRoleId;
     try {
-      JsonObject jObject = new JsonParser().parse(userInfo).getAsJsonObject();
-      userIDCreated = jObject.get("id").getAsString();
-      System.out.println("User ID is : " + userIDCreated);
+      JsonObject jObject = new JsonParser().parse(resp).getAsJsonObject();
+      returnedRoleId = jObject.get("id").getAsString();
+      LOG.debug("Ranger returns roleId: {}", roleId);
     } catch (JsonParseException e) {
       e.printStackTrace();
       throw e;
     }
-    return userIDCreated;
+    return returnedRoleId;
   }
 
-  private String getCreateGroupJsonString(String groupName) throws Exception {
-    String jsonCreateGroupString = "{ \"name\":\"" + groupName + "\"," +
-        "  \"description\":\"test\" " +
-        " }";
-    return jsonCreateGroupString;
+  private String getCreateRoleJsonStr(String roleName, String adminRoleName) {
+    return "{"
+        + "  \"name\":\"" + roleName + "\","
+        + "  \"description\":\"Role created by Ozone for Multi-Tenancy\""
+        + (adminRoleName == null ? "" : ", \"roles\":"
+        + "[{\"name\":\"" + adminRoleName + "\",\"isAdmin\": true}]")
+        + "}";
   }
 
+  public String createRole(OzoneTenantRolePrincipal role, String adminRoleName)
+      throws IOException {
 
-  public String createGroup(OzoneTenantGroupPrincipal group) throws Exception {
-    String rangerAdminUrl =
-        rangerHttpsAddress + OZONE_OM_RANGER_ADMIN_CREATE_GROUP_HTTP_ENDPOINT;
+    String endpointUrl =
+        rangerHttpsAddress + OZONE_OM_RANGER_ADMIN_CREATE_ROLE_HTTP_ENDPOINT;
 
-    String jsonCreateGroupString = getCreateGroupJsonString(group.toString());
+    String jsonData = getCreateRoleJsonStr(role.toString(), adminRoleName);
 
-    HttpsURLConnection conn = makeHttpsPostCall(rangerAdminUrl,
-        jsonCreateGroupString,
-        "POST", false);
-    String groupInfo = getResponseData(conn);
-    String groupIdCreated;
+    final HttpsURLConnection conn = makeHttpCall(endpointUrl,
+        jsonData, "POST", false);
+    if (conn.getResponseCode() != HTTP_OK) {
+      throw new IOException("Ranger REST API failure: " + conn.getResponseCode()
+          + " " + conn.getResponseMessage()
+          + ". Role name '" + role + "' likely already exists in Ranger");
+    }
+    String roleInfo = getResponseData(conn);
+    String roleId;
     try {
-      JsonObject jObject = new JsonParser().parse(groupInfo).getAsJsonObject();
-      groupIdCreated = jObject.get("id").getAsString();
-      System.out.println("GroupID is: " + groupIdCreated);
+      JsonObject jObject = new JsonParser().parse(roleInfo).getAsJsonObject();
+      roleId = jObject.get("id").getAsString();
+      LOG.debug("Ranger returned roleId: {}", roleId);
     } catch (JsonParseException e) {
       e.printStackTrace();
       throw e;
     }
-    return groupIdCreated;
+    return roleId;
   }
 
   public String createAccessPolicy(AccessPolicy policy) throws Exception {
     String rangerAdminUrl =
         rangerHttpsAddress + OZONE_OM_RANGER_ADMIN_CREATE_POLICY_HTTP_ENDPOINT;
 
-    HttpsURLConnection conn = makeHttpsPostCall(rangerAdminUrl,
+    HttpsURLConnection conn = makeHttpCall(rangerAdminUrl,
         policy.serializePolicyToJsonString(),
         "POST", false);
     String policyInfo = getResponseData(conn);
@@ -350,7 +348,7 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
     try {
       JsonObject jObject = new JsonParser().parse(policyInfo).getAsJsonObject();
       policyID = jObject.get("id").getAsString();
-      System.out.println("policyID is : " + policyID);
+      LOG.debug("policyID is: {}", policyID);
     } catch (JsonParseException e) {
       e.printStackTrace();
       throw e;
@@ -380,7 +378,7 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
         rangerHttpsAddress + OZONE_OM_RANGER_ADMIN_DELETE_USER_HTTP_ENDPOINT
             + userId + "?forceDelete=true";
 
-    HttpsURLConnection conn = makeHttpsPostCall(rangerAdminUrl, null,
+    HttpsURLConnection conn = makeHttpCall(rangerAdminUrl, null,
         "DELETE", false);
     int respnseCode = conn.getResponseCode();
     if (respnseCode != 200 && respnseCode != 204) {
@@ -388,13 +386,13 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
     }
   }
 
-  public void deleteGroup(String groupId) throws IOException {
+  public void deleteRole(String groupId) throws IOException {
 
     String rangerAdminUrl =
         rangerHttpsAddress + OZONE_OM_RANGER_ADMIN_DELETE_GROUP_HTTP_ENDPOINT
             + groupId + "?forceDelete=true";
 
-    HttpsURLConnection conn = makeHttpsPostCall(rangerAdminUrl, null,
+    HttpsURLConnection conn = makeHttpCall(rangerAdminUrl, null,
         "DELETE", false);
     int respnseCode = conn.getResponseCode();
     if (respnseCode != 200 && respnseCode != 204) {
@@ -406,7 +404,7 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
   public void deletePolicybyName(String policyName) throws Exception {
     AccessPolicy policy = getAccessPolicyByName(policyName);
     String  policyID = policy.getPolicyID();
-    System.out.println("policyID is : " + policyID);
+    LOG.debug("policyID is: {}", policyID);
     deletePolicybyId(policyID);
   }
 
@@ -416,7 +414,7 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
         rangerHttpsAddress + OZONE_OM_RANGER_ADMIN_DELETE_POLICY_HTTP_ENDPOINT
             + policyId + "?forceDelete=true";
     try {
-      HttpsURLConnection conn = makeHttpsPostCall(rangerAdminUrl, null,
+      HttpsURLConnection conn = makeHttpCall(rangerAdminUrl, null,
           "DELETE", false);
       int respnseCode = conn.getResponseCode();
       if (respnseCode != 200 && respnseCode != 204) {
@@ -431,12 +429,13 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
       throws IOException {
     StringBuilder response = new StringBuilder();
     try (BufferedReader br = new BufferedReader(
-        new InputStreamReader(urlConnection.getInputStream(), "utf-8"))) {
+        new InputStreamReader(urlConnection.getInputStream(),
+            StandardCharsets.UTF_8))) {
       String responseLine;
       while ((responseLine = br.readLine()) != null) {
         response.append(responseLine.trim());
       }
-      System.out.println(response);
+      LOG.debug("Got response: {}", response);
     } catch (Exception e) {
       e.printStackTrace();
       throw e;
@@ -444,7 +443,7 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
     return response.toString();
   }
 
-  private HttpsURLConnection makeHttpsPostCall(String urlString,
+  private HttpsURLConnection makeHttpCall(String urlString,
                                               String jsonInputString,
                                               String method, boolean isSpnego)
       throws IOException {
@@ -457,11 +456,11 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
     urlConnection.setRequestProperty("Accept", "application/json");
     urlConnection.setRequestProperty("Authorization", authHeaderValue);
 
-    if ((jsonInputString !=null) && !jsonInputString.isEmpty()) {
+    if ((jsonInputString != null) && !jsonInputString.isEmpty()) {
       urlConnection.setDoOutput(true);
       urlConnection.setRequestProperty("Content-Type", "application/json;");
       try (OutputStream os = urlConnection.getOutputStream()) {
-        byte[] input = jsonInputString.getBytes("utf-8");
+        byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
         os.write(input, 0, input.length);
         os.flush();
       }
@@ -471,8 +470,7 @@ public class MultiTenantAccessAuthorizerRangerPlugin implements
   }
 
   private HttpsURLConnection makeHttpsGetCall(String urlString,
-                                               String method, boolean isSpnego)
-      throws IOException, AuthenticationException {
+      String method, boolean isSpnego) throws IOException {
 
     URL url = new URL(urlString);
     HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/RevokeUserAccessToTenantHandler.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/OzoneOwnerPrincipal.java
similarity index 64%
copy from hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/RevokeUserAccessToTenantHandler.java
copy to hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/OzoneOwnerPrincipal.java
index 0c4cdf7..6ad51c2 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/RevokeUserAccessToTenantHandler.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/OzoneOwnerPrincipal.java
@@ -15,21 +15,25 @@
  *  See the License for the specific language governing permissions and
  *  limitations under the License.
  */
-package org.apache.hadoop.ozone.shell.tenant;
+package org.apache.hadoop.ozone.om.multitenant;
 
-import org.apache.hadoop.ozone.client.OzoneClient;
-import org.apache.hadoop.ozone.shell.OzoneAddress;
-import picocli.CommandLine;
+import java.security.Principal;
 
 /**
- * ozone tenant user revoke.
+ * Used to specify {OWNER} tag in Ranger.
  */
-@CommandLine.Command(name = "revoke",
-    description = "Revoke user access to tenant")
-public class RevokeUserAccessToTenantHandler extends TenantHandler {
+public final class OzoneOwnerPrincipal implements Principal {
+
+  public OzoneOwnerPrincipal() {
+  }
+
+  @Override
+  public String toString() {
+    return getName();
+  }
 
   @Override
-  protected void execute(OzoneClient client, OzoneAddress address) {
-    out().println("Not Implemented.");
+  public String getName() {
+    return "{OWNER}";
   }
 }
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/OzoneTenantGroupPrincipal.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/OzoneTenantRolePrincipal.java
similarity index 65%
rename from hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/OzoneTenantGroupPrincipal.java
rename to hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/OzoneTenantRolePrincipal.java
index c264ad7..3aa3e3d 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/OzoneTenantGroupPrincipal.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/OzoneTenantRolePrincipal.java
@@ -21,23 +21,23 @@ import org.apache.hadoop.ozone.OzoneConsts;
 import java.security.Principal;
 
 /**
- * Used to identify a tenant's group in Ranger.
+ * Used to identify a tenant's role in Ranger.
  */
-public final class OzoneTenantGroupPrincipal implements Principal {
+public final class OzoneTenantRolePrincipal implements Principal {
   private final String tenantID;
-  private final String groupName;
+  private final String roleName;
 
-  public static OzoneTenantGroupPrincipal newUserGroup(String tenantID) {
-    return new OzoneTenantGroupPrincipal(tenantID, "GroupTenantAllUsers");
+  public static OzoneTenantRolePrincipal getUserRole(String tenantID) {
+    return new OzoneTenantRolePrincipal(tenantID, "UserRole");
   }
 
-  public static OzoneTenantGroupPrincipal newAdminGroup(String tenantID) {
-    return new OzoneTenantGroupPrincipal(tenantID, "GroupTenantAllAdmins");
+  public static OzoneTenantRolePrincipal getAdminRole(String tenantID) {
+    return new OzoneTenantRolePrincipal(tenantID, "AdminRole");
   }
 
-  private OzoneTenantGroupPrincipal(String tenantID, String groupName) {
+  private OzoneTenantRolePrincipal(String tenantID, String roleName) {
     this.tenantID = tenantID;
-    this.groupName = groupName;
+    this.roleName = roleName;
   }
 
   public String getTenantID() {
@@ -51,6 +51,6 @@ public final class OzoneTenantGroupPrincipal implements Principal {
 
   @Override
   public String getName() {
-    return tenantID + OzoneConsts.TENANT_NAME_USER_NAME_DELIMITER + groupName;
+    return tenantID + OzoneConsts.TENANT_NAME_ROLE_DELIMITER + roleName;
   }
 }
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/RangerAccessPolicy.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/RangerAccessPolicy.java
index 113bd99..f05f237 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/RangerAccessPolicy.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/RangerAccessPolicy.java
@@ -192,8 +192,8 @@ public class RangerAccessPolicy implements AccessPolicy {
         continue;
       }
       policyItems.append("{");
-      if (list.get(0).getPrincipal() instanceof OzoneTenantGroupPrincipal) {
-        policyItems.append("\"groups\":[\"" + mapElem.getKey() + "\"],");
+      if (list.get(0).getPrincipal() instanceof OzoneTenantRolePrincipal) {
+        policyItems.append("\"roles\":[\"" + mapElem.getKey() + "\"],");
       } else {
         policyItems.append("\"users\":[\"" + mapElem.getKey() + "\"],");
       }
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/Tenant.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/Tenant.java
index cd2ab4b..73e1f29 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/Tenant.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/multitenant/Tenant.java
@@ -51,9 +51,9 @@ public interface Tenant {
 
   void removeTenantAccessPolicy(AccessPolicy policy);
 
-  void addTenantAccessGroup(String groupID);
+  void addTenantAccessRole(String groupID);
 
-  void removeTenantAccessGroup(String groupID);
+  void removeTenantAccessRole(String groupID);
 
-  List<String> getTenantGroups();
+  List<String> getTenantRoles();
 }
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
index ae5f276..9063dd5 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
@@ -47,6 +47,7 @@ import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.S3SecretValue;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
+import org.apache.hadoop.ozone.om.helpers.TenantInfoList;
 import org.apache.hadoop.ozone.om.helpers.TenantUserInfoValue;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OzoneAclInfo;
@@ -472,13 +473,38 @@ public interface OzoneManagerProtocol
    * @param accessId access ID.
    * @throws IOException
    */
-  S3SecretValue assignUserToTenant(String username, String tenantName,
+  S3SecretValue tenantAssignUserAccessId(String username, String tenantName,
       String accessId) throws IOException;
 
   OmVolumeArgs getS3Volume(String accessID) throws IOException;
 
   // TODO: modify, delete
   /**
+   * Revoke user accessId to tenant.
+   * @param accessId accessId to be revoked.
+   * @throws IOException
+   */
+  void tenantRevokeUserAccessId(String accessId) throws IOException;
+
+  /**
+   * Assign admin role to an accessId in a tenant.
+   * @param accessId access ID.
+   * @param tenantName tenant name.
+   * @param delegated true if making delegated admin.
+   * @throws IOException
+   */
+  void tenantAssignAdmin(String accessId, String tenantName,
+      boolean delegated) throws IOException;
+
+  /**
+   * Revoke admin role of an accessId from a tenant.
+   * @param accessId access ID.
+   * @param tenantName tenant name.
+   * @throws IOException
+   */
+  void tenantRevokeAdmin(String accessId, String tenantName) throws IOException;
+
+  /**
    * Get tenant info for a user.
    * @param userPrincipal Kerberos principal of a user.
    * @return TenantUserInfo
@@ -488,6 +514,13 @@ public interface OzoneManagerProtocol
       throws IOException;
 
   /**
+   * List tenants.
+   * @return TenantInfoList
+   * @throws IOException
+   */
+  TenantInfoList listTenant() throws IOException;
+
+  /**
    * OzoneFS api to get file status for an entry.
    *
    * @param keyArgs Key args
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index 94b1c9b..b01fe3a 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -56,6 +56,7 @@ import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.S3SecretValue;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
+import org.apache.hadoop.ozone.om.helpers.TenantInfoList;
 import org.apache.hadoop.ozone.om.helpers.TenantUserInfoValue;
 import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AddAclRequest;
@@ -74,8 +75,6 @@ import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CreateF
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CreateKeyRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CreateKeyResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CreateTenantRequest;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssignUserToTenantRequest;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssignUserToTenantResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CreateVolumeRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DBUpdatesRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DBUpdatesResponse;
@@ -108,6 +107,8 @@ import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListMul
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListMultipartUploadsResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListStatusRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListStatusResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListTenantRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListTenantResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListTrashRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListTrashResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListVolumeRequest;
@@ -145,6 +146,12 @@ import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.SetAclR
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.SetAclResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.SetBucketPropertyRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.SetVolumePropertyRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantAssignAdminRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantAssignUserAccessIdRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantAssignUserAccessIdResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantGetUserInfoRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantGetUserInfoResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantRevokeUserAccessIdRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.VolumeInfo;
 import org.apache.hadoop.ozone.protocolPB.OMPBHelper;
@@ -883,6 +890,8 @@ public final class OzoneManagerProtocolClientSideTranslatorPB
 
   /**
    * {@inheritDoc}
+   *
+   * TODO: Add a variant that uses OmTenantArgs?
    */
   @Override
   public void createTenant(String tenantArgs)
@@ -896,33 +905,96 @@ public final class OzoneManagerProtocolClientSideTranslatorPB
     final OMResponse omResponse = submitRequest(omRequest);
     handleError(omResponse);
   }
-  // TODO: Add a variant that uses OmTenantArgs
-  // TODO: modify, delete
 
   /**
    * {@inheritDoc}
+   *
+   * TODO: Add a variant that uses OmTenantUserArgs?
    */
   @Override
-  public S3SecretValue assignUserToTenant(
+  public S3SecretValue tenantAssignUserAccessId(
       String username, String tenantName, String accessId) throws IOException {
 
-    final AssignUserToTenantRequest request =
-        AssignUserToTenantRequest.newBuilder()
+    final TenantAssignUserAccessIdRequest request =
+        TenantAssignUserAccessIdRequest.newBuilder()
         .setTenantUsername(username)
         .setTenantName(tenantName)
         .setAccessId(accessId)
         .build();
-    final OMRequest omRequest = createOMRequest(Type.AssignUserToTenant)
-        .setAssignUserToTenantRequest(request)
+    final OMRequest omRequest = createOMRequest(Type.TenantAssignUserAccessId)
+        .setTenantAssignUserAccessIdRequest(request)
         .build();
     final OMResponse omResponse = submitRequest(omRequest);
-    final AssignUserToTenantResponse resp = handleError(omResponse)
-        .getAssignUserToTenantResponse();
+    final TenantAssignUserAccessIdResponse resp = handleError(omResponse)
+        .getTenantAssignUserAccessIdResponse();
 
     return S3SecretValue.fromProtobuf(resp.getS3Secret());
   }
-  // TODO: Add a variant that uses OmTenantUserArgs?
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void tenantRevokeUserAccessId(String accessId)
+      throws IOException {
+
+    final TenantRevokeUserAccessIdRequest request =
+        TenantRevokeUserAccessIdRequest.newBuilder()
+            .setAccessId(accessId)
+            .build();
+    final OMRequest omRequest = createOMRequest(Type.TenantRevokeUserAccessId)
+        .setTenantRevokeUserAccessIdRequest(request)
+        .build();
+    final OMResponse omResponse = submitRequest(omRequest);
+    handleError(omResponse);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void tenantAssignAdmin(String accessId, String tenantName,
+      boolean delegated) throws IOException {
+
+    final TenantAssignAdminRequest.Builder requestBuilder =
+        TenantAssignAdminRequest.newBuilder()
+        .setAccessId(accessId)
+        .setDelegated(delegated);
+    if (tenantName != null) {
+      requestBuilder.setTenantName(tenantName);
+    }
+    final TenantAssignAdminRequest request = requestBuilder.build();
+    final OMRequest omRequest = createOMRequest(Type.TenantAssignAdmin)
+        .setTenantAssignAdminRequest(request)
+        .build();
+    final OMResponse omResponse = submitRequest(omRequest);
+    handleError(omResponse);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void tenantRevokeAdmin(String accessId, String tenantName)
+      throws IOException {
+
+    final TenantRevokeAdminRequest.Builder requestBuilder =
+        TenantRevokeAdminRequest.newBuilder()
+            .setAccessId(accessId);
+    if (tenantName != null) {
+      requestBuilder.setTenantName(tenantName);
+    }
+    final TenantRevokeAdminRequest request = requestBuilder.build();
+    final OMRequest omRequest = createOMRequest(Type.TenantRevokeAdmin)
+        .setTenantRevokeAdminRequest(request)
+        .build();
+    final OMResponse omResponse = submitRequest(omRequest);
+    handleError(omResponse);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
   @Override
   public TenantUserInfoValue tenantGetUserInfo(String userPrincipal)
       throws IOException {
@@ -957,6 +1029,24 @@ public final class OzoneManagerProtocolClientSideTranslatorPB
   }
 
   /**
+   * {@inheritDoc}
+   */
+  @Override
+  public TenantInfoList listTenant() throws IOException {
+
+    final ListTenantRequest request = ListTenantRequest.newBuilder()
+        .build();
+    final OMRequest omRequest = createOMRequest(Type.ListTenant)
+        .setListTenantRequest(request)
+        .build();
+    final OMResponse omResponse = submitRequest(omRequest);
+    final ListTenantResponse resp = handleError(omResponse)
+        .getListTenantResponse();
+
+    return TenantInfoList.fromProtobuf(resp.getTenantInfoList());
+  }
+
+  /**
    * Return the proxy object underlying this protocol translator.
    *
    * @return the proxy object underlying this protocol translator.
diff --git a/hadoop-ozone/dist/src/main/compose/ozonesecure/mockserverInitialization.json b/hadoop-ozone/dist/src/main/compose/ozonesecure/mockserverInitialization.json
index e1142bc..4a54f4c 100644
--- a/hadoop-ozone/dist/src/main/compose/ozonesecure/mockserverInitialization.json
+++ b/hadoop-ozone/dist/src/main/compose/ozonesecure/mockserverInitialization.json
@@ -17,7 +17,23 @@
   },
   {
     "httpRequest": {
-      "path": "/service/xusers/secure/groups"
+      "path": "/service/roles/roles"
+    },
+    "httpResponse": {
+      "body": "{id: 222}"
+    }
+  },
+  {
+    "httpRequest": {
+      "path": "/service/roles/roles/name/tenantone-UserRole"
+    },
+    "httpResponse": {
+      "body": "{id: 222, users: []}"
+    }
+  },
+  {
+    "httpRequest": {
+      "path": "/service/roles/roles/222"
     },
     "httpResponse": {
       "body": "{id: 222}"
@@ -33,7 +49,7 @@
   },
   {
     "httpRequest": {
-      "path": "/service/public/v2/api/policy/*",
+      "path": "/service/public/v2/api/policy/*"
     },
     "httpResponse": {
       "body": "[{id: 444}]"
diff --git a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-tenant.robot b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-tenant.robot
index 93ab1f9..82e80e7 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-tenant.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-tenant.robot
@@ -33,19 +33,18 @@ Init Ranger MockServer
 Secure Tenant Create Tenant Success
 #    Run Keyword   Kinit test user     testuser     testuser.keytab
     Run Keyword         Init Ranger MockServer
-    ${output} =         Execute          ozone tenant create finance
-                        Should contain   ${output}         Created tenant 'finance'
+    ${output} =         Execute          ozone tenant create tenantone
+                        Should contain   ${output}         Created tenant 'tenantone'
 
 Secure Tenant Assign User Success
-    ${output} =         Execute          ozone tenant user assign bob@EXAMPLE.COM --tenant=finance
-                        Should contain   ${output}         Assigned 'bob@EXAMPLE.COM' to 'finance'
+    ${output} =         Execute          ozone tenant user assign bob --tenant=tenantone
+                        Should contain   ${output}         Assigned 'bob' to 'tenantone'
 
 Secure Tenant GetUserInfo Success
-    ${output} =         Execute          ozone tenant user info bob@EXAMPLE.COM
-                        Should contain   ${output}         Tenant 'finance' with accessId 'finance$bob@EXAMPLE.COM'
+    ${output} =         Execute          ozone tenant user info bob
+                        Should contain   ${output}         Tenant 'tenantone' with accessId 'tenantone$bob'
 
 Secure Tenant Assign User Failure
-    ${rc}  ${result} =  Run And Return Rc And Output  ozone tenant user assign bob@EXAMPLE.COM --tenant=nonexistenttenant
-#    Should Be True	${rc} > 0
-                        Should contain   ${result}         tenant 'nonexistenttenant' doesn't exist
+    ${rc}  ${result} =  Run And Return Rc And Output  ozone tenant user assign bob --tenant=thistenantdoesnotexist
+                        Should contain   ${result}         Tenant 'thistenantdoesnotexist' doesn't exist
 
diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/multitenant/TestMultiTenantAccessAuthorizerRangerPlugin.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/multitenant/TestMultiTenantAccessAuthorizerRangerPlugin.java
index aa28900..7532905 100644
--- a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/multitenant/TestMultiTenantAccessAuthorizerRangerPlugin.java
+++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/multitenant/TestMultiTenantAccessAuthorizerRangerPlugin.java
@@ -99,16 +99,16 @@ public class TestMultiTenantAccessAuthorizerRangerPlugin {
     omm.init(conf);
 
     try {
-      OzoneTenantGroupPrincipal group1Principal =
-          OzoneTenantGroupPrincipal.newAdminGroup("tenant1");
-      OzoneTenantGroupPrincipal group2Principal =
-          OzoneTenantGroupPrincipal.newUserGroup("tenant1");
-      groupIdsCreated.add(omm.createGroup(group1Principal));
-      groupIdsCreated.add(omm.createGroup(group2Principal));
+      OzoneTenantRolePrincipal adminRole =
+          OzoneTenantRolePrincipal.getAdminRole("tenant1-AdminRole");
+      OzoneTenantRolePrincipal userRole =
+          OzoneTenantRolePrincipal.getUserRole("tenant1-UserRole");
 
-      BasicUserPrincipal userPrincipal =
-          new BasicUserPrincipal("user1Test");
-      usersIdsCreated.add(omm.createUser(userPrincipal, groupIdsCreated));
+      BasicUserPrincipal userPrincipal = new BasicUserPrincipal("user1Test");
+      usersIdsCreated.add(
+          omm.assignUser(userPrincipal, userRole.getName(), false));
+      usersIdsCreated.add(
+          omm.assignUser(userPrincipal, adminRole.getName(), true));
 
       AccessPolicy tenant1VolumeAccessPolicy = createVolumeAccessPolicy(
           "vol1", "tenant1");
@@ -136,7 +136,7 @@ public class TestMultiTenantAccessAuthorizerRangerPlugin {
         omm.deleteUser(id);
       }
       for (String id : groupIdsCreated) {
-        omm.deleteGroup(id);
+        omm.deleteRole(id);
       }
     }
   }
@@ -153,17 +153,17 @@ public class TestMultiTenantAccessAuthorizerRangerPlugin {
 
     try {
       Assert.assertTrue(policyIdsCreated.size() == 0);
-      OzoneTenantGroupPrincipal group1Principal =
-          OzoneTenantGroupPrincipal.newAdminGroup("tenant1");
-      OzoneTenantGroupPrincipal group2Principal =
-          OzoneTenantGroupPrincipal.newUserGroup("tenant1");
-      omm.createGroup(group1Principal);
-      groupIdsCreated.add(omm.getGroupId(group1Principal));
-      omm.createGroup(group2Principal);
-      groupIdsCreated.add(omm.getGroupId(group2Principal));
+      OzoneTenantRolePrincipal group1Principal =
+          OzoneTenantRolePrincipal.getAdminRole("tenant1");
+      OzoneTenantRolePrincipal group2Principal =
+          OzoneTenantRolePrincipal.getUserRole("tenant1");
+      omm.createRole(group1Principal, null);
+      groupIdsCreated.add(omm.getRole(group1Principal));
+      omm.createRole(group2Principal, group1Principal.getName());
+      groupIdsCreated.add(omm.getRole(group2Principal));
 
       userPrincipal = new BasicUserPrincipal("user1Test");
-      omm.createUser(userPrincipal, groupIdsCreated);
+      omm.assignUser(userPrincipal, group2Principal.getName(), false);
 
       AccessPolicy tenant1VolumeAccessPolicy = createVolumeAccessPolicy(
           "vol1", "tenant1");
@@ -194,17 +194,18 @@ public class TestMultiTenantAccessAuthorizerRangerPlugin {
       String userId = omm.getUserId(userPrincipal);
       omm.deleteUser(userId);
       for (String id : groupIdsCreated) {
-        omm.deleteGroup(id);
+        omm.deleteRole(id);
       }
     }
   }
 
   private AccessPolicy createVolumeAccessPolicy(String vol, String tenant)
       throws IOException {
-    OzoneTenantGroupPrincipal principal =
-        OzoneTenantGroupPrincipal.newUserGroup(tenant);
+    OzoneTenantRolePrincipal principal =
+        OzoneTenantRolePrincipal.getUserRole(tenant);
     AccessPolicy tenantVolumeAccessPolicy = new RangerAccessPolicy(
-        principal.getName() + "VolumeAccess" + vol + "Policy");
+        // principal already contains volume name
+        principal.getName() + "VolumeAccess");
     OzoneObjInfo obj = OzoneObjInfo.Builder.newBuilder()
         .setResType(VOLUME).setStoreType(OZONE).setVolumeName(vol)
         .setBucketName("").setKeyName("").build();
@@ -217,10 +218,11 @@ public class TestMultiTenantAccessAuthorizerRangerPlugin {
 
   private AccessPolicy allowCreateBucketPolicy(String vol, String tenant)
       throws IOException {
-    OzoneTenantGroupPrincipal principal =
-        OzoneTenantGroupPrincipal.newUserGroup(tenant);
+    OzoneTenantRolePrincipal principal =
+        OzoneTenantRolePrincipal.getUserRole(tenant);
     AccessPolicy tenantVolumeAccessPolicy = new RangerAccessPolicy(
-        principal.getName() + "AllowBucketCreate" + vol + "Policy");
+        // principal already contains volume name
+        principal.getName() + "BucketAccess");
     OzoneObjInfo obj = OzoneObjInfo.Builder.newBuilder()
         .setResType(BUCKET).setStoreType(OZONE).setVolumeName(vol)
         .setBucketName("*").setKeyName("").build();
@@ -228,10 +230,11 @@ public class TestMultiTenantAccessAuthorizerRangerPlugin {
     return tenantVolumeAccessPolicy;
   }
 
+  // TODO: REMOVE THIS?
   private AccessPolicy allowAccessBucketPolicy(String vol, String bucketName,
       String tenant) throws IOException {
-    OzoneTenantGroupPrincipal principal =
-        OzoneTenantGroupPrincipal.newUserGroup(tenant);
+    OzoneTenantRolePrincipal principal =
+        OzoneTenantRolePrincipal.getUserRole(tenant);
     AccessPolicy tenantVolumeAccessPolicy = new RangerAccessPolicy(
         principal.getName() + "AllowBucketAccess" + vol + bucketName +
             "Policy");
@@ -249,8 +252,8 @@ public class TestMultiTenantAccessAuthorizerRangerPlugin {
 
   private AccessPolicy allowAccessKeyPolicy(String vol, String bucketName,
       String tenant) throws IOException {
-    OzoneTenantGroupPrincipal principal =
-        OzoneTenantGroupPrincipal.newUserGroup(tenant);
+    OzoneTenantRolePrincipal principal =
+        OzoneTenantRolePrincipal.getUserRole(tenant);
     AccessPolicy tenantVolumeAccessPolicy = new RangerAccessPolicy(
         principal.getName() + "AllowBucketKeyAccess" + vol + bucketName +
             "Policy");
diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/multitenant/TestMultiTenantVolume.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/multitenant/TestMultiTenantVolume.java
index 2bca0aa..2415285 100644
--- a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/multitenant/TestMultiTenantVolume.java
+++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/multitenant/TestMultiTenantVolume.java
@@ -99,7 +99,7 @@ public class TestMultiTenantVolume {
 
     ObjectStore store = getStoreForAccessID(accessID);
     store.createTenant(tenant);
-    store.assignUserToTenant(principal, tenant, accessID);
+    store.tenantAssignUserAccessId(principal, tenant, accessID);
 
     // S3 volume pointed to by the store should be for the tenant.
     Assert.assertEquals(tenant, store.getS3Volume().getName());
diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/shell/TestOzoneTenantShell.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/shell/TestOzoneTenantShell.java
index 8fd32ff..8204273 100644
--- a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/shell/TestOzoneTenantShell.java
+++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/shell/TestOzoneTenantShell.java
@@ -28,6 +28,8 @@ import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.hadoop.ozone.ha.ConfUtils;
 import org.apache.hadoop.ozone.om.OMConfigKeys;
 import org.apache.hadoop.ozone.om.OMMultiTenantManagerImpl;
+import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
+import org.apache.hadoop.ozone.om.multitenant.MultiTenantAccessAuthorizerRangerPlugin;
 import org.apache.hadoop.ozone.om.request.s3.tenant.OMAssignUserToTenantRequest;
 import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantCreateRequest;
 import org.apache.hadoop.ozone.shell.tenant.TenantShell;
@@ -55,6 +57,9 @@ import java.util.List;
 import java.util.UUID;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RANGER_HTTPS_ADMIN_API_PASSWD;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RANGER_HTTPS_ADMIN_API_USER;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_RANGER_HTTPS_ADDRESS_KEY;
 import static org.junit.Assert.fail;
 
 /**
@@ -97,6 +102,8 @@ public class TestOzoneTenantShell {
   private static String scmId;
   private static int numOfOMs;
 
+  private static final boolean USE_ACTUAL_RANGER = false;
+
   /**
    * Create a MiniOzoneCluster for testing with using distributed Ozone
    * handler type.
@@ -112,8 +119,16 @@ public class TestOzoneTenantShell {
 
     conf = new OzoneConfiguration();
 
-    conf.setBoolean(
-        OMMultiTenantManagerImpl.OZONE_OM_TENANT_DEV_SKIP_RANGER, true);
+    if (USE_ACTUAL_RANGER) {
+      conf.set(OZONE_RANGER_HTTPS_ADDRESS_KEY, System.getenv("RANGER_ADDRESS"));
+      conf.set(OZONE_OM_RANGER_HTTPS_ADMIN_API_USER,
+          System.getenv("RANGER_USER"));
+      conf.set(OZONE_OM_RANGER_HTTPS_ADMIN_API_PASSWD,
+          System.getenv("RANGER_PASSWD"));
+    } else {
+      conf.setBoolean(OMMultiTenantManagerImpl.OZONE_OM_TENANT_DEV_SKIP_RANGER,
+          true);
+    }
 
     String path = GenericTestUtils.getTempPath(
         TestOzoneTenantShell.class.getSimpleName());
@@ -136,9 +151,9 @@ public class TestOzoneTenantShell {
         .setScmId(scmId)
         .setOMServiceId(omServiceId)
         .setNumOfOzoneManagers(numOfOMs)
+        .withoutDatanodes()  // Remove this once we are actually writing data
         .build();
     cluster.waitForClusterToBeReady();
-//    MiniOzoneHAClusterImpl impl = (MiniOzoneOMHAClusterImpl) cluster;
   }
 
   /**
@@ -159,6 +174,14 @@ public class TestOzoneTenantShell {
   public void setup() throws UnsupportedEncodingException {
     System.setOut(new PrintStream(out, false, UTF_8.name()));
     System.setErr(new PrintStream(err, false, UTF_8.name()));
+
+    // Suppress OMNotLeaderException in the log
+    GenericTestUtils.setLogLevel(RetryInvocationHandler.LOG, Level.WARN);
+    // Enable debug logging for interested classes
+    GenericTestUtils.setLogLevel(OMTenantCreateRequest.LOG, Level.DEBUG);
+    GenericTestUtils.setLogLevel(OMAssignUserToTenantRequest.LOG, Level.DEBUG);
+    GenericTestUtils.setLogLevel(MultiTenantAccessAuthorizerRangerPlugin.LOG,
+        Level.DEBUG);
   }
 
   @After
@@ -304,35 +327,81 @@ public class TestOzoneTenantShell {
     if (exactMatch) {
       Assert.assertEquals(stringToMatch, str);
     } else {
-      Assert.assertTrue(str.contains(stringToMatch));
+      Assert.assertTrue(str, str.contains(stringToMatch));
     }
   }
 
-  /**
-   * Test tenant create, assign user and get user info.
-   */
   @Test
-  public void testOzoneTenantCreateAssignInfo() throws IOException {
+  public void testAssignAdmin() throws IOException {
 
-    // Suppress OMNotLeaderException in the log
-    GenericTestUtils.setLogLevel(RetryInvocationHandler.LOG, Level.WARN);
+    final String tenantName = "devaa";
+    final String userName = "alice";
 
-    GenericTestUtils.setLogLevel(OMTenantCreateRequest.LOG, Level.DEBUG);
-    GenericTestUtils.setLogLevel(OMAssignUserToTenantRequest.LOG, Level.DEBUG);
+    executeHA(tenantShell, new String[] {"create", tenantName});
+    checkOutput(out, "Created tenant", false);
+    checkOutput(err, "", true);
+
+    // Loop assign-revoke 3 times
+    for (int i = 0; i < 3; i++) {
+      executeHA(tenantShell, new String[] {
+          "user", "assign", userName, "--tenant=" + tenantName});
+      checkOutput(out, "export AWS_ACCESS_KEY_ID=", false);
+      checkOutput(err, "Assigned", false);
+
+      executeHA(tenantShell, new String[] {"user", "assign-admin",
+          tenantName + "$" + userName, "--tenant=" + tenantName,
+          "--delegated=true"});
+      checkOutput(out, "", true);
+      checkOutput(err, "Assigned admin", false);
+
+      // Clean up
+      executeHA(tenantShell, new String[] {
+          "user", "revoke-admin", tenantName + "$" + userName,
+          "--tenant=" + tenantName});
+      checkOutput(out, "", true);
+      checkOutput(err, "Revoked admin role of", false);
+
+      executeHA(tenantShell, new String[] {
+          "user", "revoke", tenantName + "$" + userName});
+      checkOutput(out, "", true);
+      checkOutput(err, "Revoked accessId", false);
+    }
+
+    // TODO: delete tenant is not implemented yet
+    executeHA(tenantShell, new String[] {"delete", tenantName});
+  }
+
+  /**
+   * Test tenant create, assign user, get user info, assign admin, revoke admin
+   * and revoke user flow.
+   */
+  @Test
+  public void testOzoneTenantBasicOperations() throws IOException {
 
     List<String> lines = FileUtils.readLines(AUDIT_LOG_FILE, (String)null);
     Assert.assertEquals(0, lines.size());
 
+    executeHA(tenantShell, new String[] {"list"});
+    checkOutput(out, "", true);
+    checkOutput(err, "", true);
+
     // Create tenants
     // Equivalent to `ozone tenant create finance`
     executeHA(tenantShell, new String[] {"create", "finance"});
     checkOutput(out, "Created tenant 'finance'.\n", true);
     checkOutput(err, "", true);
 
-    lines = FileUtils.readLines(AUDIT_LOG_FILE, (String)null);
-    checkOutput(lines.get(lines.size() - 1), "ret=SUCCESS", false);
+//    lines = FileUtils.readLines(AUDIT_LOG_FILE, (String)null);
+    // FIXME: The check below is unstable.
+    //  Occasionally lines.size() == 0 leads to ArrayIndexOutOfBoundsException
+    //  Likely due to audit log not flushed in time at time of check.
+//    checkOutput(lines.get(lines.size() - 1), "ret=SUCCESS", false);
+
+    // Check volume creation
+    OmVolumeArgs volArgs = cluster.getOzoneManager().getVolumeInfo("finance");
+    Assert.assertEquals("finance", volArgs.getVolume());
 
-    // Creating the tenant with the same name again should result in failure
+    // Creating the tenant with the same name again should fail
     executeHA(tenantShell, new String[] {"create", "finance"});
     checkOutput(out, "", true);
     checkOutput(err, "Failed to create tenant 'finance':"
@@ -346,38 +415,112 @@ public class TestOzoneTenantShell {
     checkOutput(out, "Created tenant 'dev'.\n", true);
     checkOutput(err, "", true);
 
-    // Assign user
-    // Equivalent to `ozone tenant user assign bob@EXAMPLE.COM --tenant=finance`
+    executeHA(tenantShell, new String[] {"ls"});
+    checkOutput(out, "dev\nfinance\nresearch\n", true);
+    checkOutput(err, "", true);
+
+    executeHA(tenantShell, new String[] {"list", "--long", "--header"});
+    // Not checking the entire output here yet
+    checkOutput(out, "Policy", false);
+    checkOutput(err, "", true);
+
+    // Assign user accessId
+    // Equivalent to `ozone tenant user assign bob --tenant=finance`
     executeHA(tenantShell, new String[] {
-        "user", "assign", "bob@EXAMPLE.COM", "--tenant=finance"});
-    checkOutput(out, "export AWS_ACCESS_KEY_ID='finance$bob@EXAMPLE.COM'\n"
+        "user", "assign", "bob", "--tenant=finance"});
+    checkOutput(out, "export AWS_ACCESS_KEY_ID='finance$bob'\n"
         + "export AWS_SECRET_ACCESS_KEY='", false);
-    checkOutput(err, "Assigned 'bob@EXAMPLE.COM' to 'finance' with accessId"
-        + " 'finance$bob@EXAMPLE.COM'.\n", true);
+    checkOutput(err, "Assigned 'bob' to 'finance' with accessId"
+        + " 'finance$bob'.\n", true);
 
     executeHA(tenantShell, new String[] {
-        "user", "assign", "bob@EXAMPLE.COM", "--tenant=research"});
-    checkOutput(out, "export AWS_ACCESS_KEY_ID='research$bob@EXAMPLE.COM'\n"
+        "user", "assign", "bob", "--tenant=research"});
+    checkOutput(out, "export AWS_ACCESS_KEY_ID='research$bob'\n"
         + "export AWS_SECRET_ACCESS_KEY='", false);
-    checkOutput(err, "Assigned 'bob@EXAMPLE.COM' to 'research' with accessId"
-        + " 'research$bob@EXAMPLE.COM'.\n", true);
+    checkOutput(err, "Assigned 'bob' to 'research' with accessId"
+        + " 'research$bob'.\n", true);
 
     executeHA(tenantShell, new String[] {
-        "user", "assign", "bob@EXAMPLE.COM", "--tenant=dev"});
-    checkOutput(out, "export AWS_ACCESS_KEY_ID='dev$bob@EXAMPLE.COM'\n"
+        "user", "assign", "bob", "--tenant=dev"});
+    checkOutput(out, "export AWS_ACCESS_KEY_ID='dev$bob'\n"
         + "export AWS_SECRET_ACCESS_KEY='", false);
-    checkOutput(err, "Assigned 'bob@EXAMPLE.COM' to 'dev' with accessId"
-        + " 'dev$bob@EXAMPLE.COM'.\n", true);
+    checkOutput(err, "Assigned 'bob' to 'dev' with accessId"
+        + " 'dev$bob'.\n", true);
 
     // Get user info
-    // Equivalent to `ozone tenant user info bob@EXAMPLE.COM`
+    // Equivalent to `ozone tenant user info bob`
+    executeHA(tenantShell, new String[] {
+        "user", "info", "bob"});
+    checkOutput(out, "User 'bob' is assigned to:\n"
+        + "- Tenant 'research' with accessId 'research$bob'\n"
+        + "- Tenant 'finance' with accessId 'finance$bob'\n"
+        + "- Tenant 'dev' with accessId 'dev$bob'\n", true);
+    checkOutput(err, "", true);
+
+    // Assign admin
+    executeHA(tenantShell, new String[] {
+        "user", "assign-admin", "dev$bob", "--tenant=dev"});
+    checkOutput(out, "", true);
+    checkOutput(err,
+        "Assigned admin to 'dev$bob' in tenant 'dev'\n", true);
+
+    executeHA(tenantShell, new String[] {
+        "user", "info", "bob"});
+    checkOutput(out, "Tenant 'dev' delegated admin with accessId", false);
+    checkOutput(err, "", true);
+
+    // Revoke admin
+    executeHA(tenantShell, new String[] {
+        "user", "revoke-admin", "dev$bob", "--tenant=dev"});
+    checkOutput(out, "", true);
+    checkOutput(err, "Revoked admin role of 'dev$bob' "
+        + "from tenant 'dev'\n", true);
+
+    executeHA(tenantShell, new String[] {
+        "user", "info", "bob"});
+    checkOutput(out, "User 'bob' is assigned to:\n"
+        + "- Tenant 'research' with accessId 'research$bob'\n"
+        + "- Tenant 'finance' with accessId 'finance$bob'\n"
+        + "- Tenant 'dev' with accessId 'dev$bob'\n", true);
+    checkOutput(err, "", true);
+
+    // Revoke user accessId
+    executeHA(tenantShell, new String[] {
+        "user", "revoke", "research$bob"});
+    checkOutput(out, "", true);
+    checkOutput(err, "Revoked accessId 'research$bob'.\n", true);
+
     executeHA(tenantShell, new String[] {
-        "user", "info", "bob@EXAMPLE.COM"});
-    checkOutput(out, "User 'bob@EXAMPLE.COM' is assigned to:\n"
-        + "- Tenant 'finance' with accessId 'finance$bob@EXAMPLE.COM'\n"
-        + "- Tenant 'research' with accessId 'research$bob@EXAMPLE.COM'\n"
-        + "- Tenant 'dev' with accessId 'dev$bob@EXAMPLE.COM'\n\n", true);
+        "user", "info", "bob"});
+    checkOutput(out, "User 'bob' is assigned to:\n"
+        + "- Tenant 'finance' with accessId 'finance$bob'\n"
+        + "- Tenant 'dev' with accessId 'dev$bob'\n", true);
     checkOutput(err, "", true);
+
+    // Assign user again
+    executeHA(tenantShell, new String[] {
+        "user", "assign", "bob", "--tenant=research"});
+    checkOutput(out, "export AWS_ACCESS_KEY_ID='research$bob'\n"
+        + "export AWS_SECRET_ACCESS_KEY='", false);
+    checkOutput(err, "Assigned 'bob' to 'research' with accessId"
+        + " 'research$bob'.\n", true);
+
+    // Attempt to assign the user to the same tenant under another accessId,
+    //  should fail.
+    executeHA(tenantShell, new String[] {
+        "user", "assign", "bob", "--tenant=research",
+        "--accessId=research$bob42"});
+    checkOutput(out, "", false);
+    checkOutput(err, "Failed to assign 'bob' to 'research': "
+        + "The same user is not allowed to be assigned to the same tenant "
+        + "more than once. User 'bob' is already assigned to tenant 'research' "
+        + "with accessId 'research$bob'.\n", true);
+
+    // Clean up
+    executeHA(tenantShell, new String[] {
+        "user", "revoke", "research$bob"});
+    checkOutput(out, "", true);
+    checkOutput(err, "Revoked accessId", false);
   }
 
 }
diff --git a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 296b4ca..6867247 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -106,11 +106,16 @@ enum Type {
   CreateTenant = 95;
   ModifyTenant = 96;
   DeleteTenant = 97;
+  ListTenant = 103;  // TODO: Renumber this when rebasing
 
   TenantGetUserInfo = 98;
-  AssignUserToTenant = 99;
-  RevokeUserAccessToTenant = 100;
-  GetS3Volume = 101;
+  TenantAssignUserAccessId = 99;
+  TenantRevokeUserAccessId = 100;
+
+  TenantAssignAdmin = 101;
+  TenantRevokeAdmin = 102;
+
+  GetS3Volume = 104;
 }
 
 message OMRequest {
@@ -194,14 +199,19 @@ message OMRequest {
 
   optional PurgePathsRequest                purgePathsRequest              = 94;
 
-  optional CreateTenantRequest              createTenantRequest            = 95;
-  optional ModifyTenantRequest              modifyTenantRequest            = 96;
-  optional DeleteTenantRequest              deleteTenantRequest            = 97;
+  optional CreateTenantRequest              CreateTenantRequest            = 95;
+  optional ModifyTenantRequest              ModifyTenantRequest            = 96;
+  optional DeleteTenantRequest              DeleteTenantRequest            = 97;
+  optional ListTenantRequest                ListTenantRequest              = 103;  // TODO: Renumber this when rebasing
 
   optional TenantGetUserInfoRequest         TenantGetUserInfoRequest       = 98;
-  optional AssignUserToTenantRequest        AssignUserToTenantRequest      = 99;
-  optional RevokeUserAccessToTenantRequest  RevokeUserAccessToTenantRequest = 100;
-  optional GetS3VolumeRequest               getS3VolumeRequest             = 101;
+  optional TenantAssignUserAccessIdRequest  TenantAssignUserAccessIdRequest= 99;
+  optional TenantRevokeUserAccessIdRequest  TenantRevokeUserAccessIdRequest= 100;
+
+  optional TenantAssignAdminRequest         TenantAssignAdminRequest       = 101;
+  optional TenantRevokeAdminRequest         TenantRevokeAdminRequest       = 102;
+
+  optional GetS3VolumeRequest               getS3VolumeRequest             = 104;
 }
 
 message OMResponse {
@@ -280,14 +290,19 @@ message OMResponse {
   optional PurgePathsResponse                 purgePathsResponse           = 93;
 
   // Skipped 94 to align with OMRequest
-  optional CreateTenantResponse              createTenantResponse          = 95;
-  optional ModifyTenantResponse              modifyTenantResponse          = 96;
-  optional DeleteTenantResponse              deleteTenantResponse          = 97;
+  optional CreateTenantResponse              CreateTenantResponse          = 95;
+  optional ModifyTenantResponse              ModifyTenantResponse          = 96;
+  optional DeleteTenantResponse              DeleteTenantResponse          = 97;
+  optional ListTenantResponse                ListTenantResponse            = 103;  // TODO: Renumber this when rebasing
 
   optional TenantGetUserInfoResponse         TenantGetUserInfoResponse     = 98;
-  optional AssignUserToTenantResponse        AssignUserToTenantResponse    = 99;
-  optional RevokeUserAccessToTenantResponse  RevokeUserAccessToTenantResponse = 100;
-  optional GetS3VolumeResponse               getS3VolumeResponse           = 101;
+  optional TenantAssignUserAccessIdResponse  TenantAssignUserAccessIdResponse= 99;
+  optional TenantRevokeUserAccessIdResponse  TenantRevokeUserAccessIdResponse= 100;
+
+  optional TenantAssignAdminResponse         TenantAssignAdminResponse     = 101;
+  optional TenantRevokeAdminResponse         TenantRevokeAdminResponse     = 102;
+
+  optional GetS3VolumeResponse               getS3VolumeResponse           = 104;
 }
 
 enum Status {
@@ -387,9 +402,10 @@ enum Status {
     TENANT_ALREADY_EXISTS = 76;
     INVALID_TENANT_NAME = 77;
 
-    TENANT_USER_NOT_FOUND = 78;
-    TENANT_USER_ALREADY_EXISTS = 79;
+    TENANT_USER_ACCESSID_NOT_FOUND = 78;
+    TENANT_USER_ACCESSID_ALREADY_EXISTS = 79;  // TODO: Remove if not used
     INVALID_TENANT_USER_NAME = 80;
+    INVALID_ACCESSID = 81;
 }
 
 /**
@@ -1362,25 +1378,45 @@ message GetS3SecretResponse {
     required S3Secret s3Secret = 2;
 }
 
-message TenantUserInfo {
-  optional string userPrincipal = 1;
-  repeated TenantAccessIdInfo accessIdInfo = 2;
+message TenantInfo {
+    optional string tenantName = 1;
+    optional string bucketNamespaceName = 2;
+    optional string accountNamespaceName = 3;
+    optional string userPolicyGroupName = 4;
+    optional string bucketPolicyGroupName = 5;
 }
 
-message TenantAccessIdInfo {
-  optional string accessId = 1;
-  optional string tenantName = 2;
+message ListTenantRequest {
+
+}
+
+message ListTenantResponse {
+    optional bool success = 1;
+    repeated TenantInfo tenantInfo = 2;
 }
 
 message TenantGetUserInfoRequest {
-  optional string userPrincipal = 1;
+    optional string userPrincipal = 1;
 }
 
 message TenantGetUserInfoResponse {
-  optional bool success = 1;
-  optional TenantUserInfo tenantUserInfo = 2;
+    optional bool success = 1;
+    optional TenantUserInfo tenantUserInfo = 2;
 }
 
+message TenantUserInfo {
+    optional string userPrincipal = 1;
+    repeated TenantAccessIdInfo accessIdInfo = 2;
+}
+
+message TenantAccessIdInfo {
+    optional string accessId = 1;
+    optional string tenantName = 2;
+    optional bool isAdmin = 3;
+    optional bool isDelegatedAdmin = 4;
+}
+
+
 message LayoutVersion {
   required uint64 version = 1;
 }
@@ -1402,13 +1438,24 @@ message DeleteTenantRequest {
     optional string tenantName = 1;
 }
 
-message AssignUserToTenantRequest {
+message TenantAssignUserAccessIdRequest {
     optional string tenantUsername = 1;
     optional string tenantName = 2;
     optional string accessId = 3;
 }
 
-message RevokeUserAccessToTenantRequest {
+message TenantRevokeUserAccessIdRequest {
+    optional string accessId = 1;
+    optional string tenantName = 2;
+}
+
+message TenantAssignAdminRequest {
+    optional string accessId = 1;
+    optional string tenantName = 2;
+    optional bool delegated = 3;
+}
+
+message TenantRevokeAdminRequest {
     optional string accessId = 1;
     optional string tenantName = 2;
 }
@@ -1429,12 +1476,20 @@ message DeleteTenantResponse {
     optional bool success = 1;
 }
 
-message AssignUserToTenantResponse {
+message TenantAssignUserAccessIdResponse {
     optional bool success = 1;
     optional S3Secret s3Secret = 2;
 }
 
-message RevokeUserAccessToTenantResponse {
+message TenantRevokeUserAccessIdResponse {
+    optional bool success = 1;
+}
+
+message TenantAssignAdminResponse {
+    optional bool success = 1;
+}
+
+message TenantRevokeAdminResponse {
     optional bool success = 1;
 }
 
diff --git a/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java b/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java
index 760cc00..74c790d 100644
--- a/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java
+++ b/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java
@@ -357,8 +357,6 @@ public interface OMMetadataManager extends DBStoreHAManager {
 
   Table<String, TransactionInfo> getTransactionInfoTable();
 
-  Table<String, String> getTenantUserTable();
-
   Table<String, OmDBAccessIdInfo> getTenantAccessIdTable();
 
   Table<String, OmDBKerberosPrincipalInfo> getPrincipalToAccessIdsTable();
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMultiTenantManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMultiTenantManager.java
index 6621417..5363e3b 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMultiTenantManager.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMultiTenantManager.java
@@ -111,14 +111,14 @@ public interface OMMultiTenantManager {
 
   /**
    * Creates a new user that exists for S3 API access to Ozone.
-   * @param tenantName
    * @param principal
+   * @param tenantName
    * @param accessID
    * @return Unique UserID.
    * @throws IOException if there is any error condition detected.
    */
   String assignUserToTenant(BasicUserPrincipal principal, String tenantName,
-                            String accessID);
+                            String accessID) throws IOException;
 
   /**
    * Given a user, destroys all state associated with that user.
@@ -127,7 +127,7 @@ public interface OMMultiTenantManager {
    * @return
    * @throws IOException
    */
-  void destroyUser(String accessID);
+  void revokeUserAccessId(String accessID) throws IOException;
 
 
   /**
@@ -177,8 +177,9 @@ public interface OMMultiTenantManager {
   /**
    * Given a user, make him an admin of the corresponding Tenant.
    * @param accessID
+   * @param delegated
    */
-  void assignTenantAdminRole(String accessID) throws IOException;
+  void assignTenantAdmin(String accessID, boolean delegated) throws IOException;
 
   /**
    * Given a user, remove him as admin of the corresponding Tenant.
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMultiTenantManagerImpl.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMultiTenantManagerImpl.java
index d7265dc..b82052e 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMultiTenantManagerImpl.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMultiTenantManagerImpl.java
@@ -18,6 +18,7 @@
 package org.apache.hadoop.ozone.om;
 
 import static org.apache.hadoop.ozone.om.multitenant.AccessPolicy.AccessGrantType.ALLOW;
+import static org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.ALL;
 import static org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.CREATE;
 import static org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.LIST;
 import static org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.NONE;
@@ -29,7 +30,7 @@ import static org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType.VOLUME;
 import static org.apache.hadoop.ozone.security.acl.OzoneObj.StoreType.OZONE;
 
 import java.io.IOException;
-import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -48,7 +49,8 @@ import org.apache.hadoop.ozone.om.multitenant.CephCompatibleTenantImpl;
 import org.apache.hadoop.ozone.om.multitenant.MultiTenantAccessAuthorizer;
 import org.apache.hadoop.ozone.om.multitenant.MultiTenantAccessAuthorizerDummyPlugin;
 import org.apache.hadoop.ozone.om.multitenant.MultiTenantAccessAuthorizerRangerPlugin;
-import org.apache.hadoop.ozone.om.multitenant.OzoneTenantGroupPrincipal;
+import org.apache.hadoop.ozone.om.multitenant.OzoneOwnerPrincipal;
+import org.apache.hadoop.ozone.om.multitenant.OzoneTenantRolePrincipal;
 import org.apache.hadoop.ozone.om.multitenant.RangerAccessPolicy;
 import org.apache.hadoop.ozone.om.multitenant.Tenant;
 import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType;
@@ -215,42 +217,45 @@ public class OMMultiTenantManagerImpl implements OMMultiTenantManager {
       // TODO : Make it an idempotent operation. If any ranger state creation
       //  fails because it already exists, Ignore it.
 
-      OzoneTenantGroupPrincipal allTenantUsers =
-          OzoneTenantGroupPrincipal.newUserGroup(tenantID);
-      String allTenantUsersGroupID = authorizer.createGroup(allTenantUsers);
-      tenant.addTenantAccessGroup(allTenantUsersGroupID);
+      // Create admin role first
+      final OzoneTenantRolePrincipal adminRole =
+          OzoneTenantRolePrincipal.getAdminRole(tenantID);
+      String adminRoleId = authorizer.createRole(adminRole, null);
+      tenant.addTenantAccessRole(adminRoleId);
 
-      OzoneTenantGroupPrincipal allTenantAdmins =
-          OzoneTenantGroupPrincipal.newAdminGroup(tenantID);
-      String allTenantAdminsGroupID = authorizer.createGroup(allTenantAdmins);
-      tenant.addTenantAccessGroup(allTenantAdminsGroupID);
+      // Then create user role, and add admin role as its delegated admin
+      final OzoneTenantRolePrincipal userRole =
+          OzoneTenantRolePrincipal.getUserRole(tenantID);
+      String userRoleId = authorizer.createRole(userRole, adminRole.getName());
+      tenant.addTenantAccessRole(userRoleId);
 
-      List<String> allTenantGroups = new ArrayList<>();
-      allTenantGroups.add(allTenantUsers.toString());
-      allTenantGroups.add(allTenantAdmins.toString());
-      inMemoryTenantToTenantGroups.put(tenantID, allTenantGroups);
+      final List<String> allTenantRole =
+          Arrays.asList(userRole.getName(), adminRole.getName());
+      inMemoryTenantToTenantGroups.put(tenantID, allTenantRole);
 
       BucketNameSpace bucketNameSpace = tenant.getTenantBucketNameSpace();
+      // bucket namespace is volume name ??
       for (OzoneObj volume : bucketNameSpace.getBucketNameSpaceObjects()) {
         String volumeName = volume.getVolumeName();
+
         // Allow Volume List access
-        AccessPolicy tenantVolumeAccessPolicy = createVolumeAccessPolicy(
-            volumeName, allTenantUsers);
+        AccessPolicy tenantVolumeAccessPolicy = newDefaultVolumeAccessPolicy(
+            volumeName, userRole, adminRole);
         tenantVolumeAccessPolicy.setPolicyID(
             authorizer.createAccessPolicy(tenantVolumeAccessPolicy));
         tenant.addTenantAccessPolicy(tenantVolumeAccessPolicy);
 
         // Allow Bucket Create within Volume
-        AccessPolicy tenantBucketCreatePolicy = allowCreateBucketPolicy(
-            volumeName, allTenantUsers);
+        AccessPolicy tenantBucketCreatePolicy =
+            newDefaultBucketAccessPolicy(volumeName, userRole);
         tenantBucketCreatePolicy.setPolicyID(
             authorizer.createAccessPolicy(tenantBucketCreatePolicy));
         tenant.addTenantAccessPolicy(tenantBucketCreatePolicy);
       }
 
       inMemoryTenantToPolicyNameListMap.put(tenantID,
-          tenant.getTenantAccessPolicies().stream().map(e->e.getPolicyName())
-              .collect(Collectors.toList()));
+          tenant.getTenantAccessPolicies().stream().map(
+              AccessPolicy::getPolicyName).collect(Collectors.toList()));
     } catch (Exception e) {
       try {
         destroyTenant(tenant);
@@ -293,8 +298,8 @@ public class OMMultiTenantManagerImpl implements OMMultiTenantManager {
       for (AccessPolicy policy : tenant.getTenantAccessPolicies()) {
         authorizer.deletePolicybyId(policy.getPolicyID());
       }
-      for (String groupID : tenant.getTenantGroups()) {
-        authorizer.deleteGroup(groupID);
+      for (String roleId : tenant.getTenantRoles()) {
+        authorizer.deleteRole(roleId);
       }
 
       inMemoryTenantNameToTenantInfoMap.remove(tenant.getTenantId());
@@ -331,10 +336,11 @@ public class OMMultiTenantManagerImpl implements OMMultiTenantManager {
    * @param tenantName
    * @param accessID
    * @return Tenant, or null on error
+   * @throws IOException
    */
   @Override
   public String assignUserToTenant(BasicUserPrincipal principal,
-      String tenantName, String accessID) {
+      String tenantName, String accessID) throws IOException {
     try {
       controlPathLock.writeLock().lock();
       Tenant tenant = getTenantInfo(tenantName);
@@ -343,29 +349,25 @@ public class OMMultiTenantManagerImpl implements OMMultiTenantManager {
             tenantName);
         return null;
       }
-      final OzoneTenantGroupPrincipal groupTenantAllUsers =
-          OzoneTenantGroupPrincipal.newUserGroup(tenantName);
-      String idGroupTenantAllUsers = authorizer.getGroupId(groupTenantAllUsers);
-      List<String> userGroupIDs = new ArrayList<>();
-      userGroupIDs.add(idGroupTenantAllUsers);
-
-      String userID = authorizer.createUser(principal, userGroupIDs);
+      final OzoneTenantRolePrincipal roleTenantAllUsers =
+          OzoneTenantRolePrincipal.getUserRole(tenantName);
+      String roleJsonStr = authorizer.getRole(roleTenantAllUsers);
+      String roleId = authorizer.assignUser(principal, roleJsonStr, false);
 
       inMemoryAccessIDToTenantNameMap.put(accessID, tenantName);
-      inMemoryAccessIDToListOfGroupsMap.put(accessID, userGroupIDs);
+//      inMemoryAccessIDToListOfGroupsMap.put(accessID, userRoleIds);
 
-      return userID;
-    } catch (Exception e) {
-      destroyUser(accessID);
-      LOG.error(e.getMessage());
-      return null;
+      return roleId;
+    } catch (IOException e) {
+      revokeUserAccessId(accessID);
+      throw e;
     } finally {
       controlPathLock.writeLock().unlock();
     }
   }
 
   @Override
-  public void destroyUser(String accessID) {
+  public void revokeUserAccessId(String accessID) throws IOException {
     try {
       controlPathLock.writeLock().lock();
       String tenantName = getTenantForAccessID(accessID);
@@ -379,8 +381,6 @@ public class OMMultiTenantManagerImpl implements OMMultiTenantManager {
 
       inMemoryAccessIDToTenantNameMap.remove(accessID);
       inMemoryAccessIDToListOfGroupsMap.remove(accessID);
-    } catch (Exception e) {
-      LOG.error(e.getMessage());
     } finally {
       controlPathLock.writeLock().unlock();
     }
@@ -438,14 +438,35 @@ public class OMMultiTenantManagerImpl implements OMMultiTenantManager {
   }
 
   @Override
-  public void assignTenantAdminRole(String accessID)
+  public void assignTenantAdmin(String accessID, boolean delegated)
       throws IOException {
-
+    try {
+      controlPathLock.writeLock().lock();
+      // tenantId (tenant name) is necessary to retrieve role name
+      final String tenantId = getTenantForAccessID(accessID);
+      assert(tenantId != null);
+
+      final OzoneTenantRolePrincipal existingAdminRole =
+          OzoneTenantRolePrincipal.getAdminRole(tenantId);
+      final String roleJsonStr = authorizer.getRole(existingAdminRole);
+      final String userPrincipal = getUserNameGivenAccessId(accessID);
+      // Add user principal (not accessId!) to the role
+      final String roleId = authorizer.assignUser(
+          new BasicUserPrincipal(userPrincipal), roleJsonStr, delegated);
+      assert(roleId != null);
+
+      // TODO: update some in-memory mappings?
+
+    } catch (IOException e) {
+      revokeTenantAdmin(accessID);
+      throw e;
+    } finally {
+      controlPathLock.writeLock().unlock();
+    }
   }
 
   @Override
-  public void revokeTenantAdmin(String accessID)
-      throws IOException {
+  public void revokeTenantAdmin(String accessID) throws IOException {
 
   }
 
@@ -509,34 +530,58 @@ public class OMMultiTenantManagerImpl implements OMMultiTenantManager {
 
   }
 
-  private AccessPolicy createVolumeAccessPolicy(String vol,
-      OzoneTenantGroupPrincipal principal) throws IOException {
-    AccessPolicy tenantVolumeAccessPolicy = new RangerAccessPolicy(
-        principal.getName() + "VolumeAccess" + vol + "Policy");
+  private AccessPolicy newDefaultVolumeAccessPolicy(String volumeName,
+      OzoneTenantRolePrincipal userPrinc, OzoneTenantRolePrincipal adminPrinc)
+      throws IOException {
+
+    AccessPolicy policy = new RangerAccessPolicy(
+        // principal already contains volume name
+        volumeName + " - VolumeAccess");
     OzoneObjInfo obj = OzoneObjInfo.Builder.newBuilder()
-        .setResType(VOLUME).setStoreType(OZONE).setVolumeName(vol)
+        .setResType(VOLUME).setStoreType(OZONE).setVolumeName(volumeName)
         .setBucketName("").setKeyName("").build();
-    tenantVolumeAccessPolicy.addAccessPolicyElem(obj, principal, READ, ALLOW);
-    tenantVolumeAccessPolicy.addAccessPolicyElem(obj, principal, LIST, ALLOW);
-    tenantVolumeAccessPolicy.addAccessPolicyElem(obj, principal,
-        READ_ACL, ALLOW);
-    return tenantVolumeAccessPolicy;
-  }
-
-  private AccessPolicy allowCreateBucketPolicy(String vol,
-      OzoneTenantGroupPrincipal principal) throws IOException {
-    AccessPolicy tenantVolumeAccessPolicy = new RangerAccessPolicy(
-        principal.getName() + "AllowBucketCreate" + vol + "Policy");
+    // Tenant users have READ, LIST and READ_ACL access on the volume
+    policy.addAccessPolicyElem(obj, userPrinc, READ, ALLOW);
+    policy.addAccessPolicyElem(obj, userPrinc, LIST, ALLOW);
+    policy.addAccessPolicyElem(obj, userPrinc, READ_ACL, ALLOW);
+    // Tenant admins have ALL access on the volume
+    policy.addAccessPolicyElem(obj, adminPrinc, ALL, ALLOW);
+    return policy;
+  }
+
+  private AccessPolicy newDefaultBucketAccessPolicy(String volumeName,
+      OzoneTenantRolePrincipal userPrinc) throws IOException {
+    AccessPolicy policy = new RangerAccessPolicy(
+        // principal already contains volume name
+        volumeName + " - BucketAccess");
     OzoneObjInfo obj = OzoneObjInfo.Builder.newBuilder()
-        .setResType(BUCKET).setStoreType(OZONE).setVolumeName(vol)
+        .setResType(BUCKET).setStoreType(OZONE).setVolumeName(volumeName)
         .setBucketName("*").setKeyName("").build();
-    tenantVolumeAccessPolicy.addAccessPolicyElem(obj, principal, CREATE, ALLOW);
-    return tenantVolumeAccessPolicy;
+    // Tenant users have permission to CREATE buckets
+    policy.addAccessPolicyElem(obj, userPrinc, CREATE, ALLOW);
+    // Bucket owner have ALL access on their own buckets. TODO: Tentative
+    policy.addAccessPolicyElem(obj, new OzoneOwnerPrincipal(), ALL, ALLOW);
+    return policy;
+  }
+
+  // TODO: Fine-tune this once we have bucket ownership.
+  private AccessPolicy newDefaultKeyAccessPolicy(String volumeName,
+      String bucketName) throws IOException {
+    AccessPolicy policy = new RangerAccessPolicy(
+        // principal already contains volume name
+        volumeName + " - KeyAccess");
+    // TODO: Double check the policy
+    OzoneObjInfo obj = OzoneObjInfo.Builder.newBuilder()
+        .setResType(KEY).setStoreType(OZONE).setVolumeName(volumeName)
+        .setBucketName("*").setKeyName("*").build();
+    // Bucket owners should have ALL permission on their keys
+    policy.addAccessPolicyElem(obj, new OzoneOwnerPrincipal(), ALL, ALLOW);
+    return policy;
   }
 
   private AccessPolicy allowAccessBucketPolicy(String vol, String bucketName,
-      OzoneTenantGroupPrincipal principal) throws IOException {
-    AccessPolicy tenantVolumeAccessPolicy = new RangerAccessPolicy(
+      OzoneTenantRolePrincipal principal) throws IOException {
+    AccessPolicy policy = new RangerAccessPolicy(
         principal.getName() + "AllowBucketAccess" + vol + bucketName +
             "Policy");
     OzoneObjInfo obj = OzoneObjInfo.Builder.newBuilder()
@@ -544,16 +589,16 @@ public class OMMultiTenantManagerImpl implements OMMultiTenantManager {
         .setBucketName(bucketName).setKeyName("*").build();
     for (ACLType acl : ACLType.values()) {
       if (acl != NONE) {
-        tenantVolumeAccessPolicy.addAccessPolicyElem(obj, principal, acl,
+        policy.addAccessPolicyElem(obj, principal, acl,
             ALLOW);
       }
     }
-    return tenantVolumeAccessPolicy;
+    return policy;
   }
 
   private AccessPolicy allowAccessKeyPolicy(String vol, String bucketName,
-      OzoneTenantGroupPrincipal principal) throws IOException {
-    AccessPolicy tenantVolumeAccessPolicy = new RangerAccessPolicy(
+      OzoneTenantRolePrincipal principal) throws IOException {
+    AccessPolicy policy = new RangerAccessPolicy(
         principal.getName() + "AllowBucketKeyAccess" + vol + bucketName +
             "Policy");
     OzoneObjInfo obj = OzoneObjInfo.Builder.newBuilder()
@@ -561,11 +606,10 @@ public class OMMultiTenantManagerImpl implements OMMultiTenantManager {
         .setBucketName(bucketName).setKeyName("*").build();
     for (ACLType acl :ACLType.values()) {
       if (acl != NONE) {
-        tenantVolumeAccessPolicy.addAccessPolicyElem(obj, principal, acl,
-            ALLOW);
+        policy.addAccessPolicyElem(obj, principal, acl, ALLOW);
       }
     }
-    return tenantVolumeAccessPolicy;
+    return policy;
   }
 
   public OzoneConfiguration getConf() {
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
index 3e0fe6c..5f1a43a 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
@@ -193,8 +193,7 @@ public class OmMetadataManagerImpl implements OMMetadataManager {
       "transactionInfoTable";
   public static final String META_TABLE = "metaTable";
 
-  // Tables for S3 multi-tenancy
-  public static final String TENANT_USER_TABLE = "tenantUserTable";
+  // Tables for multi-tenancy
   public static final String TENANT_ACCESS_ID_TABLE = "tenantAccessIdTable";
   public static final String PRINCIPAL_TO_ACCESS_IDS_TABLE =
       "principalToAccessIdsTable";
@@ -220,7 +219,6 @@ public class OmMetadataManagerImpl implements OMMetadataManager {
       DELETED_DIR_TABLE,
       OPEN_FILE_TABLE,
       META_TABLE,
-      TENANT_USER_TABLE,
       TENANT_ACCESS_ID_TABLE,
       PRINCIPAL_TO_ACCESS_IDS_TABLE,
       TENANT_STATE_TABLE,
@@ -250,8 +248,7 @@ public class OmMetadataManagerImpl implements OMMetadataManager {
   private Table transactionInfoTable;
   private Table metaTable;
 
-  // Tables for S3 multi-tenancy
-  private Table tenantUserTable;
+  // Tables for multi-tenancy
   private Table tenantAccessIdTable;
   private Table principalToAccessIdsTable;
   private Table tenantStateTable;
@@ -463,7 +460,6 @@ public class OmMetadataManagerImpl implements OMMetadataManager {
         .addTable(DELETED_DIR_TABLE)
         .addTable(TRANSACTION_INFO_TABLE)
         .addTable(META_TABLE)
-        .addTable(TENANT_USER_TABLE)
         .addTable(TENANT_ACCESS_ID_TABLE)
         .addTable(PRINCIPAL_TO_ACCESS_IDS_TABLE)
         .addTable(TENANT_STATE_TABLE)
@@ -562,12 +558,6 @@ public class OmMetadataManagerImpl implements OMMetadataManager {
     metaTable = this.store.getTable(META_TABLE, String.class, String.class);
     checkTableStatus(metaTable, META_TABLE);
 
-    // tenant user name -> tenant name string
-    // TODO: Unused. Remove.
-    tenantUserTable = this.store.getTable(TENANT_USER_TABLE,
-        String.class, String.class);
-    checkTableStatus(tenantUserTable, TENANT_USER_TABLE);
-
     // accessId -> OmDBAccessIdInfo (tenantId, secret, Kerberos principal)
     tenantAccessIdTable = this.store.getTable(TENANT_ACCESS_ID_TABLE,
         String.class, OmDBAccessIdInfo.class);
@@ -1338,11 +1328,6 @@ public class OmMetadataManagerImpl implements OMMetadataManager {
   }
 
   @Override
-  public Table<String, String> getTenantUserTable() {
-    return tenantUserTable;
-  }
-
-  @Override
   public Table<String, OmDBAccessIdInfo> getTenantAccessIdTable() {
     return tenantAccessIdTable;
   }
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index cddcb0d..114bb20 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -119,6 +119,7 @@ import org.apache.hadoop.ozone.om.helpers.OmBucketArgs;
 import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
 import org.apache.hadoop.ozone.om.helpers.OmDBAccessIdInfo;
 import org.apache.hadoop.ozone.om.helpers.OmDBKerberosPrincipalInfo;
+import org.apache.hadoop.ozone.om.helpers.OmDBTenantInfo;
 import org.apache.hadoop.ozone.om.helpers.OmDeleteKeys;
 import org.apache.hadoop.ozone.om.helpers.OmKeyArgs;
 import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
@@ -137,6 +138,7 @@ import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.S3SecretValue;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
+import org.apache.hadoop.ozone.om.helpers.TenantInfoList;
 import org.apache.hadoop.ozone.om.helpers.TenantUserInfoValue;
 import org.apache.hadoop.ozone.om.protocol.OMInterServiceProtocol;
 import org.apache.hadoop.ozone.om.protocolPB.OMInterServiceProtocolClientSideImpl;
@@ -159,6 +161,7 @@ import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRoleI
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OzoneAclInfo;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ServicePort;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantAccessIdInfo;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantInfo;
 import org.apache.hadoop.ozone.protocolPB.OMInterServiceProtocolServerSideImpl;
 import org.apache.hadoop.ozone.storage.proto.OzoneManagerStorageProtos.PersistedUserVolumeInfo;
 import org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
@@ -253,6 +256,7 @@ import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.DETE
 import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_AUTH_METHOD;
 import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
 import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.KEY_NOT_FOUND;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.PERMISSION_DENIED;
 import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.TOKEN_ERROR_OTHER;
 import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.Resource.VOLUME_LOCK;
 import static org.apache.hadoop.ozone.om.ratis.OzoneManagerRatisServer.RaftServerStatus.LEADER_AND_READY;
@@ -3077,15 +3081,77 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
         "non-Ratis createTenant() is not implemented");
   }
 
-  // TODO: modify, delete
-
   /**
-   * Assign user to tenant.
+   * Assign user accessId to tenant.
    */
-  public S3SecretValue assignUserToTenant(
+  public S3SecretValue tenantAssignUserAccessId(
       String username, String tenantName, String accessId) throws IOException {
     throw new NotImplementedException(
-        "non-Ratis assignUserToTenant() is not implemented");
+        "non-Ratis tenantAssignUserAccessId() is not implemented");
+  }
+
+  /**
+   * Revoke user accessId to tenant.
+   */
+  public void tenantRevokeUserAccessId(String accessId) throws IOException {
+    throw new NotImplementedException(
+        "non-Ratis tenantRevokeUserAccessId() is not implemented");
+  }
+
+  /**
+   * Assign admin role to an accessId in a tenant.
+   */
+  public void tenantAssignAdmin(String accessId, String tenantName,
+      boolean delegated) throws IOException {
+    throw new NotImplementedException(
+        "non-Ratis tenantAssignAdmin() is not implemented");
+  }
+
+  /**
+   * Revoke admin role of an accessId from a tenant.
+   */
+  public void tenantRevokeAdmin(String accessId, String tenantName)
+      throws IOException {
+    throw new NotImplementedException(
+        "non-Ratis tenantRevokeAdmin() is not implemented");
+  }
+
+  /**
+   * List tenants.
+   */
+  public TenantInfoList listTenant() throws IOException {
+
+    final UserGroupInformation ugi = getRemoteUser();
+    if (!isAdmin(ugi)) {
+      final OMException omEx = new OMException(
+          "Only Ozone admins are allowed to list tenants.", PERMISSION_DENIED);
+      AUDIT.logWriteFailure(buildAuditMessageForFailure(
+          OMAction.LIST_TENANT, new LinkedHashMap<>(), omEx));
+      throw omEx;
+    }
+
+    final List<TenantInfo> tenantInfoList = new ArrayList<>();
+
+    TableIterator<String, ? extends Table.KeyValue<String, OmDBTenantInfo>>
+        iterator = metadataManager.getTenantStateTable().iterator();
+
+    while (iterator.hasNext()) {
+      final Table.KeyValue<String, OmDBTenantInfo> dbEntry = iterator.next();
+      final OmDBTenantInfo omDBTenantInfo = dbEntry.getValue();
+      assert(dbEntry.getKey().equals(omDBTenantInfo.getTenantName()));
+      tenantInfoList.add(TenantInfo.newBuilder()
+          .setTenantName(omDBTenantInfo.getTenantName())
+          .setBucketNamespaceName(omDBTenantInfo.getBucketNamespaceName())
+          .setAccountNamespaceName(omDBTenantInfo.getAccountNamespaceName())
+          .setUserPolicyGroupName(omDBTenantInfo.getUserPolicyGroupName())
+          .setBucketNamespaceName(omDBTenantInfo.getBucketPolicyGroupName())
+          .build());
+    }
+
+    AUDIT.logReadSuccess(buildAuditMessageForSuccess(
+        OMAction.LIST_TENANT, new LinkedHashMap<>()));
+
+    return new TenantInfoList(tenantInfoList);
   }
 
   /**
@@ -3109,26 +3175,34 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
     final Set<String> accessIds = kerberosPrincipalInfo.getAccessIds();
 
     final Map<String, String> auditMap = new LinkedHashMap<>();
-    auditMap.put(OzoneConsts.TENANT, userPrincipal);
+    auditMap.put("userPrincipal", userPrincipal);
 
     accessIds.forEach(accessId -> {
       try {
-        // Use get() intentionally, which throws if entry doesn't exist in table
         final OmDBAccessIdInfo accessIdInfo =
             metadataManager.getTenantAccessIdTable().get(accessId);
         // Sanity check
+        if (accessIdInfo == null) {
+          LOG.error("Potential metadata error. Unexpected null accessIdInfo: "
+              + "entry for accessId '{}' doesn't exist in TenantAccessIdTable",
+              accessId);
+          throw new NullPointerException("accessIdInfo is null");
+        }
         assert(accessIdInfo.getKerberosPrincipal().equals(userPrincipal));
-        // Build TenantAccessIdInfo instances from accessId and tenantName
-        final String tenantName = accessIdInfo.getTenantId();
         accessIdInfoList.add(TenantAccessIdInfo.newBuilder()
             .setAccessId(accessId)
-            .setTenantName(tenantName)
+            .setTenantName(accessIdInfo.getTenantId())
+            .setIsAdmin(accessIdInfo.getIsAdmin())
+            .setIsDelegatedAdmin(accessIdInfo.getIsDelegatedAdmin())
             .build());
       } catch (IOException e) {
-        LOG.error("Found potential DB consistency issue! "
-            + "accessId '" + "' is supposed to exist in TenantAccessIdTable.");
+        LOG.error("Potential DB issue. Failed to retrieve OmDBAccessIdInfo "
+            + "for accessId '{}' in TenantAccessIdTable.", accessId);
+        // Audit
+        auditMap.put("accessId", accessId);
         AUDIT.logWriteFailure(buildAuditMessageForFailure(
             OMAction.TENANT_GET_USER_INFO, auditMap, e));
+        auditMap.remove("accessId");
       }
     });
 
@@ -4052,6 +4126,58 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
     }
   }
 
+  public boolean isTenantAdmin(UserGroupInformation callerUgi,
+      String tenantName, Boolean delegated) {
+    if (callerUgi == null) {
+      return false;
+    } else {
+      return isTenantAdmin(callerUgi.getShortUserName(), tenantName, delegated)
+          || isTenantAdmin(callerUgi.getUserName(), tenantName, delegated);
+    }
+  }
+
+  /**
+   * Returns true if user is a tenant's admin, false otherwise.
+   * @param username User name string.
+   * @param tenantName Tenant name string.
+   * @param delegated True if operation requires delegated admin permission.
+   */
+  public boolean isTenantAdmin(String username,
+      String tenantName, Boolean delegated) {
+    if (StringUtils.isEmpty(username) || StringUtils.isEmpty(tenantName)) {
+      return false;
+    }
+
+    try {
+      final OmDBKerberosPrincipalInfo principalInfo =
+          getMetadataManager().getPrincipalToAccessIdsTable().get(username);
+
+      if (principalInfo == null) {
+        // The user is not assigned to any tenant
+        return false;
+      }
+
+      // Find accessId assigned to the specified tenant
+      for (final String accessId : principalInfo.getAccessIds()) {
+        final OmDBAccessIdInfo accessIdInfo =
+            getMetadataManager().getTenantAccessIdTable().get(accessId);
+        if (tenantName.equals(accessIdInfo.getTenantId())) {
+          if (!delegated) {
+            return accessIdInfo.getIsAdmin();
+          } else {
+            return accessIdInfo.getIsAdmin()
+                && accessIdInfo.getIsDelegatedAdmin();
+          }
+        }
+      }
+    } catch (IOException e) {
+      LOG.error("Error while retrieving value for key '" + username
+          + "' in PrincipalToAccessIdsTable");
+    }
+
+    return false;
+  }
+
   /**
    * Returns true if OzoneNativeAuthorizer is enabled and false if otherwise.
    *
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java
index 11694c6..cb6cd6c 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java
@@ -188,17 +188,7 @@ public class OMDBDefinition implements DBDefinition {
       String.class,
       new StringCodec());
 
-  // Tables for S3 multi-tenancy
-
-  // TODO: this table will be removed with the disappearance of CreateUser API.
-  public static final DBColumnFamilyDefinition<String, String>
-            TENANT_USER_TABLE =
-            new DBColumnFamilyDefinition<>(
-                    OmMetadataManagerImpl.TENANT_USER_TABLE,
-                    String.class,
-                    new StringCodec(),
-                    String.class,
-                    new StringCodec());
+  // Tables for multi-tenancy
 
   public static final DBColumnFamilyDefinition<String, OmDBAccessIdInfo>
             TENANT_ACCESS_ID_TABLE =
@@ -274,7 +264,7 @@ public class OMDBDefinition implements DBDefinition {
         BUCKET_TABLE, MULTIPART_INFO_TABLE, PREFIX_TABLE, DTOKEN_TABLE,
         S3_SECRET_TABLE, TRANSACTION_INFO_TABLE, DIRECTORY_TABLE,
         FILE_TABLE, OPEN_FILE_TABLE, DELETED_DIR_TABLE, META_TABLE,
-        TENANT_USER_TABLE, TENANT_ACCESS_ID_TABLE,
+        TENANT_ACCESS_ID_TABLE,
         PRINCIPAL_TO_ACCESS_IDS_TABLE, TENANT_STATE_TABLE, TENANT_GROUP_TABLE,
         TENANT_ROLE_TABLE, TENANT_POLICY_TABLE };
   }
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
index ec33456..eb28310 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
@@ -79,11 +79,13 @@ import org.apache.hadoop.ozone.om.request.s3.multipart.S3MultipartUploadComplete
 import org.apache.hadoop.ozone.om.request.s3.multipart.S3MultipartUploadCompleteRequestWithFSO;
 import org.apache.hadoop.ozone.om.request.s3.security.S3GetSecretRequest;
 import org.apache.hadoop.ozone.om.request.s3.security.S3RevokeSecretRequest;
+import org.apache.hadoop.ozone.om.request.s3.tenant.OMAssignUserToTenantRequest;
+import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantAssignAdminRequest;
 import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantCreateRequest;
 import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantDeleteRequest;
 import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantModifyRequest;
-import org.apache.hadoop.ozone.om.request.s3.tenant.OMAssignUserToTenantRequest;
-import org.apache.hadoop.ozone.om.request.s3.tenant.OMRevokeUserAccessToTenantRequest;
+import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantRevokeAdminRequest;
+import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantRevokeUserAccessIdRequest;
 import org.apache.hadoop.ozone.om.request.security.OMCancelDelegationTokenRequest;
 import org.apache.hadoop.ozone.om.request.security.OMGetDelegationTokenRequest;
 import org.apache.hadoop.ozone.om.request.security.OMRenewDelegationTokenRequest;
@@ -263,10 +265,14 @@ public final class OzoneManagerRatisUtils {
       return new OMTenantModifyRequest(omRequest);
     case DeleteTenant:
       return new OMTenantDeleteRequest(omRequest);
-    case AssignUserToTenant:
+    case TenantAssignUserAccessId:
       return new OMAssignUserToTenantRequest(omRequest);
-    case RevokeUserAccessToTenant:
-      return new OMRevokeUserAccessToTenantRequest(omRequest);
+    case TenantRevokeUserAccessId:
+      return new OMTenantRevokeUserAccessIdRequest(omRequest);
+    case TenantAssignAdmin:
+      return new OMTenantAssignAdminRequest(omRequest);
+    case TenantRevokeAdmin:
+      return new OMTenantRevokeAdminRequest(omRequest);
     default:
       throw new IllegalStateException("Unrecognized write command " +
           "type request" + cmdType);
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMAssignUserToTenantRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMAssignUserToTenantRequest.java
index 8359ace..9b4a747 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMAssignUserToTenantRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMAssignUserToTenantRequest.java
@@ -35,12 +35,12 @@ import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerDoubleBufferHelper;
 import org.apache.hadoop.ozone.om.request.OMClientRequest;
 import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
 import org.apache.hadoop.ozone.om.response.OMClientResponse;
-import org.apache.hadoop.ozone.om.response.s3.tenant.OMAssignUserToTenantResponse;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssignUserToTenantRequest;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssignUserToTenantResponse;
+import org.apache.hadoop.ozone.om.response.s3.tenant.OMTenantAssignUserAccessIdResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Secret;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantAssignUserAccessIdRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantAssignUserAccessIdResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UpdateGetS3SecretRequest;
 import org.apache.http.auth.BasicUserPrincipal;
 import org.slf4j.Logger;
@@ -52,13 +52,18 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.TreeSet;
 
+import static org.apache.hadoop.ozone.om.helpers.OmDBKerberosPrincipalInfo.SERIALIZATION_SPLIT_KEY;
 import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.Resource.S3_SECRET_LOCK;
 import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.Resource.VOLUME_LOCK;
+import static org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantRequestHelper.checkTenantAdmin;
+import static org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantRequestHelper.checkTenantExistence;
 
 /*
-  Ratis execution flow for OMTenantUserCreate
+  Ratis execution flow for OMAssignUserToTenant request:
+  (might be a bit outdated)
 
 - Client (AssignUserToTenantHandler, etc.)
+  - Check admin privilege
   - Check username validity: ensure no invalid characters
   - Send request to server
 - OMAssignUserToTenantRequest
@@ -94,6 +99,8 @@ import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.Resource.VOLUME_L
 
 /**
  * Handles OMAssignUserToTenantRequest.
+ *
+ * TODO: Rename this to OMTenantAssignUserAccessIdRequest after rebase.
  */
 public class OMAssignUserToTenantRequest extends OMClientRequest {
   public static final Logger LOG =
@@ -105,12 +112,16 @@ public class OMAssignUserToTenantRequest extends OMClientRequest {
 
   @Override
   public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
-    final AssignUserToTenantRequest request =
-        getOmRequest().getAssignUserToTenantRequest();
+    final TenantAssignUserAccessIdRequest request =
+        getOmRequest().getTenantAssignUserAccessIdRequest();
+
+    final String tenantName = request.getTenantName();
+
+    // Caller should be an Ozone admin or tenant delegated admin
+    checkTenantAdmin(ozoneManager, tenantName);
 
     // Note: Tenant username _is_ the Kerberos principal of the user
     final String tenantUsername = request.getTenantUsername();
-    final String tenantName = request.getTenantName();
     final String accessId = request.getAccessId();
 
     // Check tenantUsername (user's Kerberos principal) validity. TODO: Check
@@ -127,8 +138,27 @@ public class OMAssignUserToTenantRequest extends OMClientRequest {
           OMException.ResultCodes.INVALID_TENANT_NAME);
     }
 
-    // Won't check tenant existence in preExecute.
-    // Won't check Kerberos principal existence.
+    // Check accessId validity.
+    if (accessId.contains(SERIALIZATION_SPLIT_KEY)) {
+      throw new OMException("Invalid accessId '" + accessId +
+          "'. accessId should not contain '" + SERIALIZATION_SPLIT_KEY + "'",
+          OMException.ResultCodes.INVALID_ACCESSID);
+    }
+
+    checkTenantExistence(ozoneManager.getMetadataManager(), tenantName);
+
+    // Below call implies user existence check in authorizer.
+    // If the user doesn't exist, Ranger return 400 and the call should throw.
+
+    // Call OMMTM
+    // Inform MultiTenantManager of user assignment so it could
+    //  initialize some policies in Ranger.
+    final String roleId = ozoneManager.getMultiTenantManager()
+        .assignUserToTenant(new BasicUserPrincipal(tenantUsername), tenantName,
+            accessId);
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("roleId that the user is assigned to: {}", roleId);
+    }
 
     // Generate secret. Used only when doesn't the kerberosID entry doesn't
     //  exist in DB, discarded otherwise.
@@ -153,6 +183,20 @@ public class OMAssignUserToTenantRequest extends OMClientRequest {
   }
 
   @Override
+  public void handleRequestFailure(OzoneManager ozoneManager) {
+    final TenantAssignUserAccessIdRequest request =
+        getOmRequest().getTenantAssignUserAccessIdRequest();
+
+    try {
+      // Undo Authorizer states established in preExecute
+      ozoneManager.getMultiTenantManager().revokeUserAccessId(
+          request.getAccessId());
+    } catch (Exception e) {
+      // TODO: Ignore for now. See OMTenantCreateRequest#handleRequestFailure
+    }
+  }
+
+  @Override
   @SuppressWarnings("checkstyle:methodlength")
   public OMClientResponse validateAndUpdateCache(
       OzoneManager ozoneManager, long transactionLogIndex,
@@ -172,21 +216,17 @@ public class OMAssignUserToTenantRequest extends OMClientRequest {
     Map<String, String> auditMap = new HashMap<>();
     OMMetadataManager omMetadataManager = ozoneManager.getMetadataManager();
 
-    final AssignUserToTenantRequest request =
-        getOmRequest().getAssignUserToTenantRequest();
+    final TenantAssignUserAccessIdRequest request =
+        getOmRequest().getTenantAssignUserAccessIdRequest();
     final String tenantName = request.getTenantName();
-    final String principal = request.getTenantUsername();
+    final String principal = request.getTenantUsername();  // TODO: Rename this
     assert(accessId.equals(request.getAccessId()));
-    final String volumeName = tenantName;  // TODO: Configurable
     IOException exception = null;
-    String userId;
 
-    try {
-      // Check ACL: requires ozone admin or tenant admin permission
-//      if (ozoneManager.getAclsEnabled()) {
-//        // TODO: Call OMMultiTenantManager?
-//      }
+    final String volumeName = OMTenantRequestHelper.getTenantVolumeName(
+        omMetadataManager, tenantName);
 
+    try {
       acquiredVolumeLock = omMetadataManager.getLock().acquireWriteLock(
           VOLUME_LOCK, volumeName);
 
@@ -201,13 +241,36 @@ public class OMAssignUserToTenantRequest extends OMClientRequest {
       if (omMetadataManager.getTenantAccessIdTable().isExist(accessId)) {
         LOG.error("accessId {} already exists", accessId);
         throw new OMException("accessId '" + accessId + "' already exists!",
-            OMException.ResultCodes.TENANT_USER_ALREADY_EXISTS);
+            OMException.ResultCodes.TENANT_USER_ACCESSID_ALREADY_EXISTS);
+      }
+
+      OmDBKerberosPrincipalInfo principalInfo = omMetadataManager
+          .getPrincipalToAccessIdsTable().getIfExist(principal);
+      // Reject if the user is already assigned to the tenant
+      if (principalInfo != null) {
+        // If any existing accessIds are assigned to the same tenant, throw ex
+        // TODO: There is room for perf improvement. add a map in OMMTM.
+        for (final String existingAccId : principalInfo.getAccessIds()) {
+          final OmDBAccessIdInfo accessIdInfo =
+              omMetadataManager.getTenantAccessIdTable().get(existingAccId);
+          if (accessIdInfo == null) {
+            LOG.error("Metadata error: accessIdInfo is null for accessId '{}'. "
+                + "Ignoring.", existingAccId);
+            throw new NullPointerException("accessIdInfo is null");
+          }
+          if (tenantName.equals(accessIdInfo.getTenantId())) {
+            throw new OMException("The same user is not allowed to be assigned "
+                + "to the same tenant more than once. User '" + principal
+                + "' is already assigned to tenant '" + tenantName + "' with "
+                + "accessId '" + existingAccId + "'.",
+                OMException.ResultCodes.TENANT_USER_ACCESSID_ALREADY_EXISTS);
+          }
+        }
       }
 
-      // Add to S3SecretTable.
-      // TODO: dedupe - S3GetSecretRequest
+      // Add to S3SecretTable. TODO: Remove later.
       acquiredS3SecretLock = omMetadataManager.getLock()
-          .acquireWriteLock(S3_SECRET_LOCK, principal);
+          .acquireWriteLock(S3_SECRET_LOCK, accessId);
 
       // Expect accessId absence from S3SecretTable
       // TODO: This table might be merged with tenantAccessIdTable later.
@@ -225,47 +288,36 @@ public class OMAssignUserToTenantRequest extends OMClientRequest {
           new CacheKey<>(accessId),
           new CacheValue<>(Optional.of(s3SecretValue), transactionLogIndex));
 
-      omMetadataManager.getLock().releaseWriteLock(S3_SECRET_LOCK, principal);
+      omMetadataManager.getLock().releaseWriteLock(S3_SECRET_LOCK, accessId);
       acquiredS3SecretLock = false;
 
-      // Inform MultiTenantManager of user assignment so it could
-      //  initialize some policies in Ranger.
-      // TODO: Is userId from MultiTenantManager still useful?
-      // TODO: Move this to preExecute as well.
-      userId = ozoneManager.getMultiTenantManager()
-          .assignUserToTenant(new BasicUserPrincipal(principal), tenantName,
-              accessId);
-      if (LOG.isDebugEnabled()) {
-        LOG.debug("userId for assign user to tenant request = {}", userId);
-      }
-
       // Add to tenantAccessIdTable
       final OmDBAccessIdInfo omDBAccessIdInfo = new OmDBAccessIdInfo.Builder()
-          .setTenantName(tenantName)
+          .setTenantId(tenantName)
           .setKerberosPrincipal(principal)
           .setSharedSecret(s3SecretValue.getAwsSecret())
+          .setIsAdmin(false)
+          .setIsDelegatedAdmin(false)
           .build();
       omMetadataManager.getTenantAccessIdTable().addCacheEntry(
           new CacheKey<>(accessId),
           new CacheValue<>(Optional.of(omDBAccessIdInfo), transactionLogIndex));
 
       // Add to principalToAccessIdsTable
-      OmDBKerberosPrincipalInfo omDBKerberosPrincipalInfo = omMetadataManager
-          .getPrincipalToAccessIdsTable().getIfExist(principal);
-
-      if (omDBKerberosPrincipalInfo == null) {
-        omDBKerberosPrincipalInfo = new OmDBKerberosPrincipalInfo.Builder()
+      if (principalInfo == null) {
+        principalInfo = new OmDBKerberosPrincipalInfo.Builder()
             .setAccessIds(new TreeSet<>(Collections.singleton(accessId)))
             .build();
       } else {
-        omDBKerberosPrincipalInfo.addAccessId(accessId);
+        principalInfo.addAccessId(accessId);
       }
       omMetadataManager.getPrincipalToAccessIdsTable().addCacheEntry(
           new CacheKey<>(principal),
-          new CacheValue<>(Optional.of(omDBKerberosPrincipalInfo),
+          new CacheValue<>(Optional.of(principalInfo),
               transactionLogIndex));
 
       // Add to tenantGroupTable
+      // TODO: DOUBLE CHECK GROUP NAME USAGE
       final String defaultGroupName =
           tenantName + OzoneConsts.DEFAULT_TENANT_USER_GROUP_SUFFIX;
       omMetadataManager.getTenantGroupTable().addCacheEntry(
@@ -273,26 +325,30 @@ public class OMAssignUserToTenantRequest extends OMClientRequest {
           new CacheValue<>(Optional.of(defaultGroupName), transactionLogIndex));
 
       // Add to tenantRoleTable
-      final String roleName = "role_admin";
+      // TODO: DOUBLE CHECK ROLENAME
+      final String roleName = "user";
       omMetadataManager.getTenantRoleTable().addCacheEntry(
           new CacheKey<>(accessId),
           new CacheValue<>(Optional.of(roleName), transactionLogIndex));
 
-      omResponse.setAssignUserToTenantResponse(
-          AssignUserToTenantResponse.newBuilder().setSuccess(true)
+      // Generate response
+      omResponse.setTenantAssignUserAccessIdResponse(
+          TenantAssignUserAccessIdResponse.newBuilder().setSuccess(true)
               .setS3Secret(S3Secret.newBuilder()
                   .setAwsSecret(awsSecret).setKerberosID(accessId))
               .build());
-      omClientResponse = new OMAssignUserToTenantResponse(omResponse.build(),
-          s3SecretValue, principal, defaultGroupName, roleName,
-          accessId, omDBAccessIdInfo, omDBKerberosPrincipalInfo);
+      omClientResponse = new OMTenantAssignUserAccessIdResponse(
+          omResponse.build(), s3SecretValue, principal, defaultGroupName,
+          roleName, accessId, omDBAccessIdInfo, principalInfo);
     } catch (IOException ex) {
-      ozoneManager.getMultiTenantManager().destroyUser(accessId);
+      // Error handling
+      handleRequestFailure(ozoneManager);
       exception = ex;
       // Set response success flag to false
-      omResponse.setAssignUserToTenantResponse(
-          AssignUserToTenantResponse.newBuilder().setSuccess(false).build());
-      omClientResponse = new OMAssignUserToTenantResponse(
+      omResponse.setTenantAssignUserAccessIdResponse(
+          TenantAssignUserAccessIdResponse.newBuilder()
+              .setSuccess(false).build());
+      omClientResponse = new OMTenantAssignUserAccessIdResponse(
           createErrorOMResponse(omResponse, ex));
     } finally {
       if (omClientResponse != null) {
@@ -300,7 +356,7 @@ public class OMAssignUserToTenantRequest extends OMClientRequest {
             .add(omClientResponse, transactionLogIndex));
       }
       if (acquiredS3SecretLock) {
-        omMetadataManager.getLock().releaseWriteLock(S3_SECRET_LOCK, principal);
+        omMetadataManager.getLock().releaseWriteLock(S3_SECRET_LOCK, accessId);
       }
       if (acquiredVolumeLock) {
         omMetadataManager.getLock().releaseWriteLock(VOLUME_LOCK, volumeName);
@@ -309,19 +365,21 @@ public class OMAssignUserToTenantRequest extends OMClientRequest {
 
     // Audit
     auditMap.put(OzoneConsts.TENANT, tenantName);
-    auditLog(ozoneManager.getAuditLogger(),
-        buildAuditMessage(OMAction.ASSIGN_USER_TO_TENANT, auditMap, exception,
+    auditMap.put("user", principal);
+    auditMap.put("accessId", accessId);
+    auditLog(ozoneManager.getAuditLogger(), buildAuditMessage(
+        OMAction.TENANT_ASSIGN_USER_ACCESSID, auditMap, exception,
             getOmRequest().getUserInfo()));
 
     if (exception == null) {
       LOG.info("Assigned user '{}' to tenant '{}' with accessId '{}'",
           principal, tenantName, accessId);
-      // TODO: omMetrics.incNumTenantUsers()
+      // TODO: omMetrics.incNumTenantAssignUser()
     } else {
       LOG.error("Failed to assign '{}' to tenant '{}' with accessId '{}': {}",
           principal, tenantName, accessId, exception.getMessage());
       // TODO: Check if the exception message is sufficient.
-      // TODO: omMetrics.incNumTenantUserCreateFails()
+      // TODO: omMetrics.incNumTenantAssignUserFails()
     }
     return omClientResponse;
   }
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMRevokeUserAccessToTenantRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMRevokeUserAccessToTenantRequest.java
deleted file mode 100644
index c542304..0000000
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMRevokeUserAccessToTenantRequest.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-package org.apache.hadoop.ozone.om.request.s3.tenant;
-
-import org.apache.hadoop.ozone.om.OzoneManager;
-import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerDoubleBufferHelper;
-import org.apache.hadoop.ozone.om.request.volume.OMVolumeRequest;
-import org.apache.hadoop.ozone.om.response.OMClientResponse;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
-
-import java.io.IOException;
-
-/**
- * Handles OMTenantUserModify request.
- */
-public class OMRevokeUserAccessToTenantRequest extends OMVolumeRequest {
-
-  public OMRevokeUserAccessToTenantRequest(OMRequest omRequest) {
-    super(omRequest);
-  }
-
-  @Override
-  public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
-    return getOmRequest();
-  }
-
-  @Override
-  public OMClientResponse validateAndUpdateCache(
-      OzoneManager ozoneManager, long transactionLogIndex,
-      OzoneManagerDoubleBufferHelper ozoneManagerDoubleBufferHelper) {
-
-    return null;
-  }
-}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantAssignAdminRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantAssignAdminRequest.java
new file mode 100644
index 0000000..dbd2fa5
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantAssignAdminRequest.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.hadoop.ozone.om.request.s3.tenant;
+
+import com.google.common.base.Optional;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
+import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.audit.OMAction;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.helpers.OmDBAccessIdInfo;
+import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerDoubleBufferHelper;
+import org.apache.hadoop.ozone.om.request.OMClientRequest;
+import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.om.response.s3.tenant.OMTenantAssignAdminResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantAssignAdminRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantAssignAdminResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.Resource.VOLUME_LOCK;
+
+/*
+  Execution flow
+
+  - preExecute
+    - Check caller admin privilege
+  - validateAndUpdateCache
+    - Update tenantAccessIdTable
+ */
+
+/**
+ * Handles OMTenantAssignAdminRequest.
+ */
+public class OMTenantAssignAdminRequest extends OMClientRequest {
+  public static final Logger LOG =
+      LoggerFactory.getLogger(OMTenantAssignAdminRequest.class);
+
+  public OMTenantAssignAdminRequest(OMRequest omRequest) {
+    super(omRequest);
+  }
+
+  @Override
+  public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
+    final TenantAssignAdminRequest request =
+        getOmRequest().getTenantAssignAdminRequest();
+
+    final String accessId = request.getAccessId();
+    String tenantName = request.getTenantName();
+
+    // If tenantName is not provided, figure it out from the table
+    if (StringUtils.isEmpty(tenantName)) {
+      tenantName = OMTenantRequestHelper.getTenantNameFromAccessId(
+          ozoneManager.getMetadataManager(), accessId);
+      assert(tenantName != null);
+    }
+
+    // Caller should be an Ozone admin or this tenant's delegated admin
+    OMTenantRequestHelper.checkTenantAdmin(ozoneManager, tenantName);
+
+    // TODO: Check tenant existence?
+
+    OmDBAccessIdInfo accessIdInfo = ozoneManager.getMetadataManager()
+        .getTenantAccessIdTable().get(accessId);
+
+    if (accessIdInfo == null) {
+      throw new OMException("accessId '" + accessId + "' not found.",
+          OMException.ResultCodes.TENANT_USER_ACCESSID_NOT_FOUND);
+    }
+
+    // Check if accessId is assigned to the tenant
+    if (!accessIdInfo.getTenantId().equals(tenantName)) {
+      throw new OMException("accessId '" + accessId +
+          "' must be assigned to tenant '" + tenantName + "' first.",
+          OMException.ResultCodes.INVALID_TENANT_NAME);
+    }
+
+    final boolean delegated;
+    if (request.hasDelegated()) {
+      delegated = request.getDelegated();
+    } else {
+      delegated = true;
+    }
+    // Call OMMTM to add user to tenant admin role
+    ozoneManager.getMultiTenantManager().assignTenantAdmin(
+        request.getAccessId(), delegated);
+
+    final OMRequest.Builder omRequestBuilder = getOmRequest().toBuilder()
+        .setUserInfo(getUserInfo())
+        .setTenantAssignAdminRequest(
+            TenantAssignAdminRequest.newBuilder()
+                .setAccessId(accessId)
+                .setTenantName(tenantName)
+                .setDelegated(delegated)
+                .build())
+        .setCmdType(getOmRequest().getCmdType())
+        .setClientId(getOmRequest().getClientId());
+
+    if (getOmRequest().hasTraceID()) {
+      omRequestBuilder.setTraceID(getOmRequest().getTraceID());
+    }
+
+    return omRequestBuilder.build();
+  }
+
+  @Override
+  public void handleRequestFailure(OzoneManager ozoneManager) {
+    final TenantAssignAdminRequest request =
+        getOmRequest().getTenantAssignAdminRequest();
+
+    try {
+      ozoneManager.getMultiTenantManager().revokeTenantAdmin(
+          request.getAccessId());
+    } catch (Exception e) {
+      // TODO: Ignore for now. See OMTenantCreateRequest#handleRequestFailure
+    }
+  }
+
+  @Override
+  @SuppressWarnings("checkstyle:methodlength")
+  public OMClientResponse validateAndUpdateCache(
+      OzoneManager ozoneManager, long transactionLogIndex,
+      OzoneManagerDoubleBufferHelper ozoneManagerDoubleBufferHelper) {
+
+    OMClientResponse omClientResponse = null;
+    final OMResponse.Builder omResponse =
+        OmResponseUtil.getOMResponseBuilder(getOmRequest());
+
+    final Map<String, String> auditMap = new HashMap<>();
+    OMMetadataManager omMetadataManager = ozoneManager.getMetadataManager();
+
+    final TenantAssignAdminRequest request =
+        getOmRequest().getTenantAssignAdminRequest();
+    final String accessId = request.getAccessId();
+    final String tenantName = request.getTenantName();
+    final boolean delegated = request.getDelegated();
+
+    boolean acquiredVolumeLock = false;  // TODO: use tenant lock instead, maybe
+    IOException exception = null;
+
+    final String volumeName = OMTenantRequestHelper.getTenantVolumeName(
+        omMetadataManager, tenantName);
+
+    try {
+      acquiredVolumeLock = omMetadataManager.getLock().acquireWriteLock(
+          VOLUME_LOCK, volumeName);
+
+      final OmDBAccessIdInfo oldAccessIdInfo =
+          omMetadataManager.getTenantAccessIdTable().get(accessId);
+
+      if (oldAccessIdInfo == null) {
+        throw new OMException("OmDBAccessIdInfo entry is missing for accessId '"
+            + accessId + "'.", OMException.ResultCodes.METADATA_ERROR);
+      }
+
+      assert(oldAccessIdInfo.getTenantId().equals(tenantName));
+
+      // Update tenantAccessIdTable
+      final OmDBAccessIdInfo newOmDBAccessIdInfo =
+          new OmDBAccessIdInfo.Builder()
+          .setTenantId(oldAccessIdInfo.getTenantId())
+          .setKerberosPrincipal(oldAccessIdInfo.getKerberosPrincipal())
+          .setSharedSecret(oldAccessIdInfo.getSharedSecret())
+          .setIsAdmin(true)
+          .setIsDelegatedAdmin(delegated)
+          .build();
+      omMetadataManager.getTenantAccessIdTable().addCacheEntry(
+          new CacheKey<>(accessId),
+          new CacheValue<>(Optional.of(newOmDBAccessIdInfo),
+              transactionLogIndex));
+
+      // Update tenantRoleTable?
+//      final String roleName = "role_admin";
+//      omMetadataManager.getTenantRoleTable().addCacheEntry(
+//          new CacheKey<>(accessId),
+//          new CacheValue<>(Optional.of(roleName), transactionLogIndex));
+
+      omResponse.setTenantAssignAdminResponse(
+          TenantAssignAdminResponse.newBuilder().setSuccess(true).build());
+      omClientResponse = new OMTenantAssignAdminResponse(omResponse.build(),
+          accessId, newOmDBAccessIdInfo);
+
+    } catch (IOException ex) {
+      // Error handling
+      handleRequestFailure(ozoneManager);
+      exception = ex;
+      // Set success flag to false
+      omResponse.setTenantAssignAdminResponse(
+          TenantAssignAdminResponse.newBuilder().setSuccess(false).build());
+      omClientResponse = new OMTenantAssignAdminResponse(
+          createErrorOMResponse(omResponse, ex));
+    } finally {
+      if (omClientResponse != null) {
+        omClientResponse.setFlushFuture(ozoneManagerDoubleBufferHelper
+            .add(omClientResponse, transactionLogIndex));
+      }
+      if (acquiredVolumeLock) {
+        omMetadataManager.getLock().releaseWriteLock(VOLUME_LOCK, volumeName);
+      }
+    }
+
+    // Audit
+    auditMap.put(OzoneConsts.TENANT, tenantName);
+    auditLog(ozoneManager.getAuditLogger(), buildAuditMessage(
+        OMAction.TENANT_ASSIGN_ADMIN, auditMap, exception,
+        getOmRequest().getUserInfo()));
+
+    if (exception == null) {
+      LOG.info("Assigned admin to accessId '{}' in tenant '{}', "
+              + "delegated: {}", accessId, tenantName, delegated);
+      // TODO: omMetrics.incNumTenantAssignAdmin()
+    } else {
+      LOG.error("Failed to assign admin to accessId '{}' in tenant '{}', "
+              + "delegated: {}: {}",
+          accessId, tenantName, delegated, exception.getMessage());
+      // TODO: omMetrics.incNumTenantAssignAdminFails()
+    }
+    return omClientResponse;
+  }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantCreateRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantCreateRequest.java
index 4cd62ee..49f329c 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantCreateRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantCreateRequest.java
@@ -111,6 +111,9 @@ public class OMTenantCreateRequest extends OMVolumeRequest {
     final CreateTenantRequest request = getOmRequest().getCreateTenantRequest();
     final String tenantName = request.getTenantName();
 
+    // Check Ozone admin privilege
+    OMTenantRequestHelper.checkAdmin(ozoneManager);
+
     // Check tenantName validity
     if (tenantName.contains(OzoneConsts.TENANT_NAME_USER_NAME_DELIMITER)) {
       throw new OMException("Invalid tenant name " + tenantName +
@@ -184,7 +187,7 @@ public class OMTenantCreateRequest extends OMVolumeRequest {
 
   @Override
   public void handleRequestFailure(OzoneManager ozoneManager) {
-    CreateTenantRequest request = getOmRequest().getCreateTenantRequest();
+    final CreateTenantRequest request = getOmRequest().getCreateTenantRequest();
 
     try {
       final Tenant tenant = ozoneManager.getMultiTenantManager()
@@ -223,13 +226,10 @@ public class OMTenantCreateRequest extends OMVolumeRequest {
     IOException exception = null;
 
     final String tenantDefaultPolicies = request.getTenantDefaultPolicyName();
-    assert(tenantDefaultPolicies != null);
-
-    LOG.debug("Processing tenant '{}' create request", tenantName);
-    LOG.debug("tenantDefaultPolicies = {}", tenantDefaultPolicies);
 
     try {
-      // Check ACL: requires volume create permission. TODO: tenant create perm?
+      // Check ACL: requires volume create permission.
+      // TODO: do we need a tenant create permission ? probably not
       if (ozoneManager.getAclsEnabled()) {
         checkAcls(ozoneManager, OzoneObj.ResourceType.VOLUME,
             OzoneObj.StoreType.OZONE, IAccessAuthorizer.ACLType.CREATE,
@@ -251,7 +251,7 @@ public class OMTenantCreateRequest extends OMVolumeRequest {
 
       // Add to tenantStateTable. Redundant assignment for clarity
       final String bucketNamespaceName = tenantName;
-      final String accountNamespaceName = tenantName;
+      final String accountNamespaceName = volumeName;
       final String userPolicyGroupName =
           tenantName + OzoneConsts.DEFAULT_TENANT_USER_POLICY_SUFFIX;
       final String bucketPolicyGroupName =
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantRequestHelper.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantRequestHelper.java
new file mode 100644
index 0000000..9b03762
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantRequestHelper.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.hadoop.ozone.om.request.s3.tenant;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.ipc.ProtobufRpcEngine;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.helpers.OmDBAccessIdInfo;
+import org.apache.hadoop.ozone.om.helpers.OmDBTenantInfo;
+import org.apache.hadoop.security.UserGroupInformation;
+
+import java.io.IOException;
+
+/**
+ * Utility class that contains helper methods for OM tenant requests.
+ */
+public final class OMTenantRequestHelper {
+
+  private OMTenantRequestHelper() {
+  }
+
+  /**
+   * Passes check only when caller is an Ozone admin,
+   * throws OMException otherwise.
+   * @throws OMException PERMISSION_DENIED
+   */
+  static void checkAdmin(OzoneManager ozoneManager) throws OMException {
+
+    final UserGroupInformation ugi = ProtobufRpcEngine.Server.getRemoteUser();
+    if (!ozoneManager.isAdmin(ugi)) {
+      throw new OMException("User '" + ugi.getUserName() +
+          "' is not an Ozone admin.",
+          OMException.ResultCodes.PERMISSION_DENIED);
+    }
+  }
+
+  /**
+   * Passes check if caller is an Ozone cluster admin or tenant delegated admin,
+   * throws OMException otherwise.
+   * @throws OMException PERMISSION_DENIED
+   */
+  static void checkTenantAdmin(OzoneManager ozoneManager, String tenantName)
+      throws OMException {
+
+    final UserGroupInformation ugi = ProtobufRpcEngine.Server.getRemoteUser();
+    if (!ozoneManager.isAdmin(ugi) &&
+        !ozoneManager.isTenantAdmin(ugi, tenantName, true)) {
+      throw new OMException("User '" + ugi.getUserName() +
+          "' is neither an Ozone admin nor a delegated admin of tenant '" +
+          tenantName + "'.", OMException.ResultCodes.PERMISSION_DENIED);
+    }
+  }
+
+  static void checkTenantExistence(OMMetadataManager omMetadataManager,
+      String tenantName) throws OMException {
+
+    try {
+      if (!omMetadataManager.getTenantStateTable().isExist(tenantName)) {
+        throw new OMException("Tenant '" + tenantName + "' doesn't exist.",
+            OMException.ResultCodes.TENANT_NOT_FOUND);
+      }
+    } catch (IOException ex) {
+      if (ex instanceof OMException) {
+        final OMException omEx = (OMException) ex;
+        if (omEx.getResult().equals(OMException.ResultCodes.TENANT_NOT_FOUND)) {
+          throw omEx;
+        }
+      }
+      throw new OMException("Unable to retrieve "
+          + "OmDBTenantInfo entry for tenant '" + tenantName + "': "
+          + ex.getMessage(), OMException.ResultCodes.METADATA_ERROR);
+    }
+  }
+
+  /**
+   * Retrieve volume name of the tenant.
+   */
+  static String getTenantVolumeName(OMMetadataManager omMetadataManager,
+      String tenantName) {
+
+    final OmDBTenantInfo tenantInfo;
+    try {
+      tenantInfo = omMetadataManager.getTenantStateTable().get(tenantName);
+    } catch (IOException e) {
+      throw new RuntimeException("Potential DB error. Unable to retrieve "
+          + "OmDBTenantInfo entry for tenant '" + tenantName + "'.");
+    }
+
+    if (tenantInfo == null) {
+      throw new RuntimeException("Potential DB error or race condition. "
+          + "OmDBTenantInfo entry is missing for tenant '" + tenantName + "'.");
+    }
+
+    final String volumeName = tenantInfo.getAccountNamespaceName();
+
+    if (StringUtils.isEmpty(tenantName)) {
+      throw new RuntimeException("Potential DB error. volumeName "
+          + "field is null or empty for tenantId '" + tenantName + "'.");
+    }
+
+    return volumeName;
+  }
+
+  static String getTenantNameFromAccessId(OMMetadataManager omMetadataManager,
+      String accessId) throws IOException {
+
+    final OmDBAccessIdInfo accessIdInfo = omMetadataManager
+        .getTenantAccessIdTable().get(accessId);
+
+    if (accessIdInfo == null) {
+      throw new OMException("OmDBAccessIdInfo entry is missing for accessId '" +
+          accessId + "'.", OMException.ResultCodes.METADATA_ERROR);
+    }
+
+    final String tenantName = accessIdInfo.getTenantId();
+
+    if (StringUtils.isEmpty(tenantName)) {
+      throw new OMException("tenantId field is null or empty for accessId '" +
+          accessId + "'.", OMException.ResultCodes.METADATA_ERROR);
+    }
+
+    return tenantName;
+  }
+
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantRevokeAdminRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantRevokeAdminRequest.java
new file mode 100644
index 0000000..705dd89
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantRevokeAdminRequest.java
@@ -0,0 +1,221 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.hadoop.ozone.om.request.s3.tenant;
+
+import com.google.common.base.Optional;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
+import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.audit.OMAction;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.helpers.OmDBAccessIdInfo;
+import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerDoubleBufferHelper;
+import org.apache.hadoop.ozone.om.request.OMClientRequest;
+import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.om.response.s3.tenant.OMTenantRevokeAdminResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantRevokeAdminRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantRevokeAdminResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.Resource.VOLUME_LOCK;
+
+/*
+  Execution flow
+
+  - preExecute
+    - Check caller admin privilege
+  - validateAndUpdateCache
+    - Update tenantAccessIdTable
+ */
+
+/**
+ * Handles OMTenantRevokeAdminRequest.
+ */
+public class OMTenantRevokeAdminRequest extends OMClientRequest {
+  public static final Logger LOG =
+      LoggerFactory.getLogger(OMTenantRevokeAdminRequest.class);
+
+  public OMTenantRevokeAdminRequest(OMRequest omRequest) {
+    super(omRequest);
+  }
+
+  @Override
+  public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
+    final TenantRevokeAdminRequest request =
+        getOmRequest().getTenantRevokeAdminRequest();
+
+    final String accessId = request.getAccessId();
+    String tenantName = request.getTenantName();
+
+    // If tenantName is not provided, figure it out from the table
+    if (StringUtils.isEmpty(tenantName)) {
+      tenantName = OMTenantRequestHelper.getTenantNameFromAccessId(
+          ozoneManager.getMetadataManager(), accessId);
+    }
+
+    // Caller should be an Ozone admin or this tenant's delegated admin
+    OMTenantRequestHelper.checkTenantAdmin(ozoneManager, tenantName);
+
+    // TODO: Check tenant existence?
+
+    OmDBAccessIdInfo accessIdInfo = ozoneManager.getMetadataManager()
+        .getTenantAccessIdTable().get(accessId);
+
+    if (accessIdInfo == null) {
+      throw new OMException("accessId '" + accessId + "' not found.",
+          OMException.ResultCodes.TENANT_USER_ACCESSID_NOT_FOUND);
+    }
+
+    // Check if accessId is assigned to the tenant
+    if (!accessIdInfo.getTenantId().equals(tenantName)) {
+      throw new OMException("accessId '" + accessId +
+          "' must be assigned to tenant '" + tenantName + "' first.",
+          OMException.ResultCodes.INVALID_TENANT_NAME);
+    }
+
+    // TODO: Call OMMTM to remove user from admin group of the tenant.
+    // The call should remove user (not accessId) from the tenant's admin group
+//    ozoneManager.getMultiTenantManager().revokeTenantAdmin();
+
+    final OMRequest.Builder omRequestBuilder = getOmRequest().toBuilder()
+        .setUserInfo(getUserInfo())
+        .setTenantRevokeAdminRequest(request)
+        .setCmdType(getOmRequest().getCmdType())
+        .setClientId(getOmRequest().getClientId());
+
+    if (getOmRequest().hasTraceID()) {
+      omRequestBuilder.setTraceID(getOmRequest().getTraceID());
+    }
+
+    return omRequestBuilder.build();
+  }
+
+  @Override
+  @SuppressWarnings("checkstyle:methodlength")
+  public OMClientResponse validateAndUpdateCache(
+      OzoneManager ozoneManager, long transactionLogIndex,
+      OzoneManagerDoubleBufferHelper ozoneManagerDoubleBufferHelper) {
+
+    OMClientResponse omClientResponse = null;
+    final OMResponse.Builder omResponse =
+        OmResponseUtil.getOMResponseBuilder(getOmRequest());
+
+    final Map<String, String> auditMap = new HashMap<>();
+    OMMetadataManager omMetadataManager = ozoneManager.getMetadataManager();
+
+    final TenantRevokeAdminRequest request =
+        getOmRequest().getTenantRevokeAdminRequest();
+    final String accessId = request.getAccessId();
+    final String tenantName = request.getTenantName();
+
+    boolean acquiredVolumeLock = false;  // TODO: use tenant lock instead, maybe
+    IOException exception = null;
+
+    final String volumeName = OMTenantRequestHelper.getTenantVolumeName(
+        omMetadataManager, tenantName);
+
+    try {
+      acquiredVolumeLock = omMetadataManager.getLock().acquireWriteLock(
+          VOLUME_LOCK, volumeName);
+
+      final OmDBAccessIdInfo oldAccessIdInfo =
+          omMetadataManager.getTenantAccessIdTable().get(accessId);
+
+      if (oldAccessIdInfo == null) {
+        throw new OMException("OmDBAccessIdInfo entry is missing for accessId '"
+            + accessId + "'.", OMException.ResultCodes.METADATA_ERROR);
+      }
+
+      assert(oldAccessIdInfo.getTenantId().equals(tenantName));
+
+      // Update tenantAccessIdTable
+      final OmDBAccessIdInfo newOmDBAccessIdInfo =
+          new OmDBAccessIdInfo.Builder()
+          .setTenantId(oldAccessIdInfo.getTenantId())
+          .setKerberosPrincipal(oldAccessIdInfo.getKerberosPrincipal())
+          .setSharedSecret(oldAccessIdInfo.getSharedSecret())
+          .setIsAdmin(false)
+          .setIsDelegatedAdmin(false)
+          .build();
+      omMetadataManager.getTenantAccessIdTable().addCacheEntry(
+          new CacheKey<>(accessId),
+          new CacheValue<>(Optional.of(newOmDBAccessIdInfo),
+              transactionLogIndex));
+
+      // Update tenantRoleTable?
+//      final String roleName = "role_admin";
+//      omMetadataManager.getTenantRoleTable().addCacheEntry(
+//          new CacheKey<>(accessId),
+//          new CacheValue<>(Optional.of(roleName), transactionLogIndex));
+
+      omResponse.setTenantRevokeAdminResponse(
+          TenantRevokeAdminResponse.newBuilder()
+              .setSuccess(true).build());
+      omClientResponse = new OMTenantRevokeAdminResponse(omResponse.build(),
+          accessId, newOmDBAccessIdInfo);
+
+    } catch (IOException ex) {
+      // Error handling: do nothing to Authorizer (Ranger) here?
+
+      exception = ex;
+      // Set success flag to false
+      omResponse.setTenantRevokeAdminResponse(
+          TenantRevokeAdminResponse.newBuilder()
+              .setSuccess(false).build());
+      omClientResponse = new OMTenantRevokeAdminResponse(
+          createErrorOMResponse(omResponse, ex));
+    } finally {
+      if (omClientResponse != null) {
+        omClientResponse.setFlushFuture(ozoneManagerDoubleBufferHelper
+            .add(omClientResponse, transactionLogIndex));
+      }
+      if (acquiredVolumeLock) {
+        omMetadataManager.getLock().releaseWriteLock(VOLUME_LOCK, volumeName);
+      }
+    }
+
+    // Audit
+    auditMap.put(OzoneConsts.TENANT, tenantName);
+    auditLog(ozoneManager.getAuditLogger(), buildAuditMessage(
+        OMAction.TENANT_REVOKE_ADMIN, auditMap, exception,
+        getOmRequest().getUserInfo()));
+
+    if (exception == null) {
+      LOG.info("Revoked admin of accessId '{}' from tenant '{}'",
+          accessId, tenantName);
+      // TODO: omMetrics.incNumTenantRevokeAdmin()
+    } else {
+      LOG.error("Failed to revoke admin of accessId '{}' from tenant '{}': {}",
+          accessId, tenantName, exception.getMessage());
+      // TODO: omMetrics.incNumTenantRevokeAdminFails()
+    }
+    return omClientResponse;
+  }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantRevokeUserAccessIdRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantRevokeUserAccessIdRequest.java
new file mode 100644
index 0000000..e364f12
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/tenant/OMTenantRevokeUserAccessIdRequest.java
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.hadoop.ozone.om.request.s3.tenant;
+
+import com.google.common.base.Optional;
+import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
+import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.audit.OMAction;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.helpers.OmDBAccessIdInfo;
+import org.apache.hadoop.ozone.om.helpers.OmDBKerberosPrincipalInfo;
+import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerDoubleBufferHelper;
+import org.apache.hadoop.ozone.om.request.OMClientRequest;
+import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.om.response.s3.tenant.OMTenantRevokeUserAccessIdResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantRevokeUserAccessIdRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantRevokeUserAccessIdResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.Resource.S3_SECRET_LOCK;
+import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.Resource.VOLUME_LOCK;
+
+/*
+  Execution flow
+
+  - preExecute
+    - Check accessId existence
+    - Get tenantName from accessId
+    - Check caller Ozone admin or tenant admin privilege
+    - Throw if accessId is a tenant admin
+    - Call Authorizer
+  - validateAndUpdateCache
+    - Update DB tables
+ */
+
+/**
+ * Handles OMTenantRevokeUserAccessIdRequest request.
+ */
+public class OMTenantRevokeUserAccessIdRequest extends OMClientRequest {
+  public static final Logger LOG = LoggerFactory.getLogger(
+      OMTenantRevokeUserAccessIdRequest.class);
+
+  public OMTenantRevokeUserAccessIdRequest(OMRequest omRequest) {
+    super(omRequest);
+  }
+
+  @Override
+  public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
+    final TenantRevokeUserAccessIdRequest request =
+        getOmRequest().getTenantRevokeUserAccessIdRequest();
+
+    final String accessId = request.getAccessId();
+
+    // As of now, OMTenantRevokeUserAccessIdRequest does not get tenantName
+    //  from the client, we just get it from the OM DB table. Uncomment
+    //  below if we want the request to be similar to OMTenantRevokeAdminRequest
+//    String tenantName = request.getTenantName();
+//    if (tenantName == null) {
+//    }
+
+    final OMMetadataManager omMetadataManager =
+        ozoneManager.getMetadataManager();
+    final OmDBAccessIdInfo accessIdInfo = omMetadataManager
+        .getTenantAccessIdTable().get(accessId);
+
+    if (accessIdInfo == null) {
+      // Note: This potentially leaks which accessIds exists in OM.
+      throw new OMException("accessId '" + accessId + "' doesn't exist",
+          OMException.ResultCodes.TENANT_USER_ACCESSID_NOT_FOUND);
+    }
+
+    final String tenantName = accessIdInfo.getTenantId();
+    assert(tenantName != null);
+    assert(tenantName.length() > 0);
+
+    // Caller should be an Ozone admin or this tenant's delegated admin
+    OMTenantRequestHelper.checkTenantAdmin(ozoneManager, tenantName);
+
+    if (accessIdInfo.getIsAdmin()) {
+      throw new OMException("accessId '" + accessId + "' is tenant admin of '" +
+          tenantName + "'. Revoke admin first.",
+          OMException.ResultCodes.PERMISSION_DENIED);
+    }
+
+    // Call OMMTM to revoke user access to tenant
+    // TODO: DOUBLE CHECK destroyUser() behavior
+    ozoneManager.getMultiTenantManager().revokeUserAccessId(accessId);
+
+    final OMRequest.Builder omRequestBuilder = getOmRequest().toBuilder()
+        .setUserInfo(getUserInfo())
+        .setTenantRevokeUserAccessIdRequest(
+            TenantRevokeUserAccessIdRequest.newBuilder()
+                .setAccessId(accessId)
+                .setTenantName(tenantName)
+                .build())
+        .setCmdType(getOmRequest().getCmdType())
+        .setClientId(getOmRequest().getClientId());
+
+    if (getOmRequest().hasTraceID()) {
+      omRequestBuilder.setTraceID(getOmRequest().getTraceID());
+    }
+
+    return omRequestBuilder.build();
+  }
+
+  @Override
+  public OMClientResponse validateAndUpdateCache(
+      OzoneManager ozoneManager, long transactionLogIndex,
+      OzoneManagerDoubleBufferHelper ozoneManagerDoubleBufferHelper) {
+
+    OMClientResponse omClientResponse = null;
+    final OzoneManagerProtocolProtos.OMResponse.Builder omResponse =
+        OmResponseUtil.getOMResponseBuilder(getOmRequest());
+
+    final Map<String, String> auditMap = new HashMap<>();
+    OMMetadataManager omMetadataManager = ozoneManager.getMetadataManager();
+
+    final TenantRevokeUserAccessIdRequest request =
+        getOmRequest().getTenantRevokeUserAccessIdRequest();
+    final String accessId = request.getAccessId();
+    final String tenantName = request.getTenantName();
+
+    boolean acquiredVolumeLock = false;
+    boolean acquiredS3SecretLock = false;
+    IOException exception = null;
+
+    final String volumeName = OMTenantRequestHelper.getTenantVolumeName(
+        omMetadataManager, tenantName);
+    String userPrincipal = null;
+
+    try {
+      acquiredVolumeLock =
+          omMetadataManager.getLock().acquireWriteLock(VOLUME_LOCK, volumeName);
+
+      // Remove from S3SecretTable. TODO: Remove later.
+      acquiredS3SecretLock = omMetadataManager.getLock()
+          .acquireWriteLock(S3_SECRET_LOCK, accessId);
+      omMetadataManager.getS3SecretTable().addCacheEntry(
+          new CacheKey<>(accessId),
+          new CacheValue<>(Optional.absent(), transactionLogIndex));
+      omMetadataManager.getLock().releaseWriteLock(S3_SECRET_LOCK,
+          accessId);
+      acquiredS3SecretLock = false;
+
+      // Remove accessId from principalToAccessIdsTable
+      OmDBAccessIdInfo omDBAccessIdInfo =
+          omMetadataManager.getTenantAccessIdTable().get(accessId);
+      assert(omDBAccessIdInfo != null);
+      userPrincipal = omDBAccessIdInfo.getKerberosPrincipal();
+      assert(userPrincipal != null);
+      OmDBKerberosPrincipalInfo principalInfo = omMetadataManager
+          .getPrincipalToAccessIdsTable().getIfExist(userPrincipal);
+      assert(principalInfo != null);
+      principalInfo.removeAccessId(accessId);
+      omMetadataManager.getPrincipalToAccessIdsTable().addCacheEntry(
+          new CacheKey<>(userPrincipal),
+          new CacheValue<>(principalInfo.getAccessIds().size() > 0 ?
+              // Invalidate (remove) the entry if accessIds set is empty
+              Optional.of(principalInfo) : Optional.absent(),
+              transactionLogIndex));
+
+      // Remove from TenantAccessIdTable
+      omMetadataManager.getTenantAccessIdTable().addCacheEntry(
+          new CacheKey<>(accessId),
+          new CacheValue<>(Optional.absent(), transactionLogIndex));
+
+      // Remove from tenantGroupTable
+      omMetadataManager.getTenantGroupTable().addCacheEntry(
+          new CacheKey<>(accessId),
+          new CacheValue<>(Optional.absent(), transactionLogIndex));
+
+      // Remove from tenantRoleTable
+      omMetadataManager.getTenantRoleTable().addCacheEntry(
+          new CacheKey<>(accessId),
+          new CacheValue<>(Optional.absent(), transactionLogIndex));
+
+      // Generate response
+      omResponse.setTenantRevokeUserAccessIdResponse(
+          TenantRevokeUserAccessIdResponse.newBuilder().setSuccess(true).build()
+      );
+      omClientResponse = new OMTenantRevokeUserAccessIdResponse(
+          omResponse.build(), accessId, userPrincipal, principalInfo);
+    } catch (IOException ex) {
+      // Error handling: do nothing to Authorizer here?
+      exception = ex;
+      // Set response success flag to false
+      omResponse.setTenantRevokeUserAccessIdResponse(
+          TenantRevokeUserAccessIdResponse.newBuilder()
+              .setSuccess(false).build());
+      omClientResponse = new OMTenantRevokeUserAccessIdResponse(
+          createErrorOMResponse(omResponse, ex));
+    } finally {
+      if (omClientResponse != null) {
+        omClientResponse.setFlushFuture(ozoneManagerDoubleBufferHelper
+            .add(omClientResponse, transactionLogIndex));
+      }
+      if (acquiredS3SecretLock) {
+        omMetadataManager.getLock().releaseWriteLock(S3_SECRET_LOCK, accessId);
+      }
+      if (acquiredVolumeLock) {
+        omMetadataManager.getLock().releaseWriteLock(VOLUME_LOCK, volumeName);
+      }
+    }
+
+    // Audit
+    auditMap.put(OzoneConsts.TENANT, tenantName);
+    auditMap.put("accessId", accessId);
+    auditMap.put("user", userPrincipal);
+    auditLog(ozoneManager.getAuditLogger(), buildAuditMessage(
+        OMAction.TENANT_REVOKE_USER_ACCESSID, auditMap, exception,
+        getOmRequest().getUserInfo()));
+
+    if (exception == null) {
+      LOG.info("Revoked user '{}' accessId '{}' to tenant '{}'",
+          userPrincipal, accessId, tenantName);
+      // TODO: omMetrics.incNumTenantRevokeUser()
+    } else {
+      LOG.error("Failed to revoke user '{}' accessId '{}' to tenant '{}': {}",
+          userPrincipal, accessId, tenantName, exception.getMessage());
+      // TODO: omMetrics.incNumTenantRevokeUserFails()
+    }
+    return omClientResponse;
+  }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantAssignAdminResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantAssignAdminResponse.java
new file mode 100644
index 0000000..7b5c4a4
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantAssignAdminResponse.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.hadoop.ozone.om.response.s3.tenant;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.hadoop.hdds.utils.db.BatchOperation;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.helpers.OmDBAccessIdInfo;
+import org.apache.hadoop.ozone.om.response.CleanupTableInfo;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+
+import static org.apache.hadoop.ozone.om.OmMetadataManagerImpl.TENANT_ACCESS_ID_TABLE;
+
+/**
+ * Response for OMTenantAssignAdminRequest.
+ */
+@CleanupTableInfo(cleanupTables = {
+    TENANT_ACCESS_ID_TABLE
+//    TENANT_ROLE_TABLE
+})
+public class OMTenantAssignAdminResponse extends OMClientResponse {
+
+  private String accessId;
+  private OmDBAccessIdInfo omDBAccessIdInfo;
+
+  public OMTenantAssignAdminResponse(@Nonnull OMResponse omResponse,
+      @Nonnull String accessId,
+      @Nonnull OmDBAccessIdInfo omDBAccessIdInfo
+  ) {
+    super(omResponse);
+    this.accessId = accessId;
+    this.omDBAccessIdInfo = omDBAccessIdInfo;
+  }
+
+  /**
+   * For when the request is not successful.
+   * For a successful request, the other constructor should be used.
+   */
+  public OMTenantAssignAdminResponse(@Nonnull OMResponse omResponse) {
+    super(omResponse);
+    checkStatusNotOK();
+  }
+
+  @Override
+  public void addToDBBatch(OMMetadataManager omMetadataManager,
+      BatchOperation batchOperation) throws IOException {
+
+    omMetadataManager.getTenantAccessIdTable().putWithBatch(
+        batchOperation, accessId, omDBAccessIdInfo);
+  }
+
+  @VisibleForTesting
+  public OmDBAccessIdInfo getOmDBAccessIdInfo() {
+    return omDBAccessIdInfo;
+  }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMAssignUserToTenantResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantAssignUserAccessIdResponse.java
similarity index 92%
copy from hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMAssignUserToTenantResponse.java
copy to hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantAssignUserAccessIdResponse.java
index f8e1faf..eb9ec84 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMAssignUserToTenantResponse.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantAssignUserAccessIdResponse.java
@@ -48,7 +48,7 @@ import static org.apache.hadoop.ozone.om.OmMetadataManagerImpl.TENANT_ROLE_TABLE
     TENANT_GROUP_TABLE,
     TENANT_ROLE_TABLE
 })
-public class OMAssignUserToTenantResponse extends OMClientResponse {
+public class OMTenantAssignUserAccessIdResponse extends OMClientResponse {
 
   private S3SecretValue s3SecretValue;
   private String principal, groupName, roleName, accessId;
@@ -56,7 +56,7 @@ public class OMAssignUserToTenantResponse extends OMClientResponse {
   private OmDBKerberosPrincipalInfo omDBKerberosPrincipalInfo;
 
   @SuppressWarnings("checkstyle:parameternumber")
-  public OMAssignUserToTenantResponse(@Nonnull OMResponse omResponse,
+  public OMTenantAssignUserAccessIdResponse(@Nonnull OMResponse omResponse,
       @Nonnull S3SecretValue s3SecretValue,
       @Nonnull String principal,
       @Nonnull String groupName,
@@ -79,7 +79,7 @@ public class OMAssignUserToTenantResponse extends OMClientResponse {
    * For when the request is not successful.
    * For a successful request, the other constructor should be used.
    */
-  public OMAssignUserToTenantResponse(@Nonnull OMResponse omResponse) {
+  public OMTenantAssignUserAccessIdResponse(@Nonnull OMResponse omResponse) {
     super(omResponse);
     checkStatusNotOK();
   }
@@ -101,9 +101,9 @@ public class OMAssignUserToTenantResponse extends OMClientResponse {
     omMetadataManager.getPrincipalToAccessIdsTable().putWithBatch(
         batchOperation, principal, omDBKerberosPrincipalInfo);
     omMetadataManager.getTenantGroupTable().putWithBatch(
-        batchOperation, principal, groupName);
+        batchOperation, accessId, groupName);
     omMetadataManager.getTenantRoleTable().putWithBatch(
-        batchOperation, principal, roleName);
+        batchOperation, accessId, roleName);
   }
 
   @VisibleForTesting
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantRevokeAdminResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantRevokeAdminResponse.java
new file mode 100644
index 0000000..d2faa36
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantRevokeAdminResponse.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.hadoop.ozone.om.response.s3.tenant;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.hadoop.hdds.utils.db.BatchOperation;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.helpers.OmDBAccessIdInfo;
+import org.apache.hadoop.ozone.om.response.CleanupTableInfo;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+
+import static org.apache.hadoop.ozone.om.OmMetadataManagerImpl.TENANT_ACCESS_ID_TABLE;
+
+/**
+ * Response for OMTenantAssignAdminRequest.
+ */
+@CleanupTableInfo(cleanupTables = {
+    TENANT_ACCESS_ID_TABLE
+//    TENANT_ROLE_TABLE
+})
+public class OMTenantRevokeAdminResponse extends OMClientResponse {
+
+  private String accessId;
+  private OmDBAccessIdInfo omDBAccessIdInfo;
+
+  public OMTenantRevokeAdminResponse(@Nonnull OMResponse omResponse,
+      @Nonnull String accessId,
+      @Nonnull OmDBAccessIdInfo omDBAccessIdInfo
+  ) {
+    super(omResponse);
+    this.accessId = accessId;
+    this.omDBAccessIdInfo = omDBAccessIdInfo;
+  }
+
+  /**
+   * For when the request is not successful.
+   * For a successful request, the other constructor should be used.
+   */
+  public OMTenantRevokeAdminResponse(@Nonnull OMResponse omResponse) {
+    super(omResponse);
+    checkStatusNotOK();
+  }
+
+  @Override
+  public void addToDBBatch(OMMetadataManager omMetadataManager,
+      BatchOperation batchOperation) throws IOException {
+
+    omMetadataManager.getTenantAccessIdTable().putWithBatch(
+        batchOperation, accessId, omDBAccessIdInfo);
+  }
+
+  @VisibleForTesting
+  public OmDBAccessIdInfo getOmDBAccessIdInfo() {
+    return omDBAccessIdInfo;
+  }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMAssignUserToTenantResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantRevokeUserAccessIdResponse.java
similarity index 61%
rename from hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMAssignUserToTenantResponse.java
rename to hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantRevokeUserAccessIdResponse.java
index f8e1faf..f3f8dac 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMAssignUserToTenantResponse.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/tenant/OMTenantRevokeUserAccessIdResponse.java
@@ -18,12 +18,9 @@
  */
 package org.apache.hadoop.ozone.om.response.s3.tenant;
 
-import com.google.common.annotations.VisibleForTesting;
 import org.apache.hadoop.hdds.utils.db.BatchOperation;
 import org.apache.hadoop.ozone.om.OMMetadataManager;
-import org.apache.hadoop.ozone.om.helpers.OmDBAccessIdInfo;
 import org.apache.hadoop.ozone.om.helpers.OmDBKerberosPrincipalInfo;
-import org.apache.hadoop.ozone.om.helpers.S3SecretValue;
 import org.apache.hadoop.ozone.om.response.CleanupTableInfo;
 import org.apache.hadoop.ozone.om.response.OMClientResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
@@ -39,7 +36,7 @@ import static org.apache.hadoop.ozone.om.OmMetadataManagerImpl.TENANT_GROUP_TABL
 import static org.apache.hadoop.ozone.om.OmMetadataManagerImpl.TENANT_ROLE_TABLE;
 
 /**
- * Response for OMAssignUserToTenantRequest.
+ * Response for OMTenantRevokeUserAccessIdRequest.
  */
 @CleanupTableInfo(cleanupTables = {
     S3_SECRET_TABLE,
@@ -48,30 +45,20 @@ import static org.apache.hadoop.ozone.om.OmMetadataManagerImpl.TENANT_ROLE_TABLE
     TENANT_GROUP_TABLE,
     TENANT_ROLE_TABLE
 })
-public class OMAssignUserToTenantResponse extends OMClientResponse {
+public class OMTenantRevokeUserAccessIdResponse extends OMClientResponse {
 
-  private S3SecretValue s3SecretValue;
-  private String principal, groupName, roleName, accessId;
-  private OmDBAccessIdInfo omDBAccessIdInfo;
+  private String principal, accessId;
   private OmDBKerberosPrincipalInfo omDBKerberosPrincipalInfo;
 
   @SuppressWarnings("checkstyle:parameternumber")
-  public OMAssignUserToTenantResponse(@Nonnull OMResponse omResponse,
-      @Nonnull S3SecretValue s3SecretValue,
-      @Nonnull String principal,
-      @Nonnull String groupName,
-      @Nonnull String roleName,
+  public OMTenantRevokeUserAccessIdResponse(@Nonnull OMResponse omResponse,
       @Nonnull String accessId,
-      @Nonnull OmDBAccessIdInfo omDBAccessIdInfo,
+      @Nonnull String principal,
       @Nonnull OmDBKerberosPrincipalInfo omDBKerberosPrincipalInfo
   ) {
     super(omResponse);
-    this.s3SecretValue = s3SecretValue;
     this.principal = principal;
-    this.groupName = groupName;
-    this.roleName = roleName;
     this.accessId = accessId;
-    this.omDBAccessIdInfo = omDBAccessIdInfo;
     this.omDBKerberosPrincipalInfo = omDBKerberosPrincipalInfo;
   }
 
@@ -79,7 +66,7 @@ public class OMAssignUserToTenantResponse extends OMClientResponse {
    * For when the request is not successful.
    * For a successful request, the other constructor should be used.
    */
-  public OMAssignUserToTenantResponse(@Nonnull OMResponse omResponse) {
+  public OMTenantRevokeUserAccessIdResponse(@Nonnull OMResponse omResponse) {
     super(omResponse);
     checkStatusNotOK();
   }
@@ -88,26 +75,27 @@ public class OMAssignUserToTenantResponse extends OMClientResponse {
   public void addToDBBatch(OMMetadataManager omMetadataManager,
       BatchOperation batchOperation) throws IOException {
 
-    if (s3SecretValue != null &&
-        getOMResponse().getStatus() == OzoneManagerProtocolProtos.Status.OK) {
-      assert(accessId.equals(s3SecretValue.getKerberosID()));
-      // Add S3SecretTable entry
-      omMetadataManager.getS3SecretTable().putWithBatch(batchOperation,
-          accessId, s3SecretValue);
+    assert(accessId != null);
+    // TODO: redundant check? Is status always OK when addToDBBatch is called
+    if (getOMResponse().getStatus() == OzoneManagerProtocolProtos.Status.OK) {
+      omMetadataManager.getS3SecretTable().deleteWithBatch(batchOperation,
+          accessId);
     }
 
-    omMetadataManager.getTenantAccessIdTable().putWithBatch(
-        batchOperation, accessId, omDBAccessIdInfo);
-    omMetadataManager.getPrincipalToAccessIdsTable().putWithBatch(
-        batchOperation, principal, omDBKerberosPrincipalInfo);
-    omMetadataManager.getTenantGroupTable().putWithBatch(
-        batchOperation, principal, groupName);
-    omMetadataManager.getTenantRoleTable().putWithBatch(
-        batchOperation, principal, roleName);
-  }
+    omMetadataManager.getTenantAccessIdTable().deleteWithBatch(
+        batchOperation, accessId);
+    omMetadataManager.getTenantGroupTable().deleteWithBatch(
+        batchOperation, accessId);
+    omMetadataManager.getTenantRoleTable().deleteWithBatch(
+        batchOperation, accessId);
 
-  @VisibleForTesting
-  public OmDBAccessIdInfo getOmDBAccessIdInfo() {
-    return omDBAccessIdInfo;
+    if (omDBKerberosPrincipalInfo.getAccessIds().size() > 0) {
+      omMetadataManager.getPrincipalToAccessIdsTable().putWithBatch(
+          batchOperation, principal, omDBKerberosPrincipalInfo);
+    } else {
+      // Remove entry from DB if accessId set is empty
+      omMetadataManager.getPrincipalToAccessIdsTable().deleteWithBatch(
+          batchOperation, principal);
+    }
   }
 }
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
index ad313d9..532b268 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
@@ -43,6 +43,7 @@ import org.apache.hadoop.ozone.om.helpers.OzoneFileStatus;
 import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
+import org.apache.hadoop.ozone.om.helpers.TenantInfoList;
 import org.apache.hadoop.ozone.om.helpers.TenantUserInfoValue;
 import org.apache.hadoop.ozone.om.ratis.OzoneManagerDoubleBuffer;
 import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerRatisUtils;
@@ -67,6 +68,8 @@ import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListBuc
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListBucketsResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListKeysRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListKeysResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListTenantRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListTenantResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListTrashRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListTrashResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ListVolumeRequest;
@@ -236,6 +239,11 @@ public class OzoneManagerRequestHandler implements RequestHandler {
             request.getTenantGetUserInfoRequest());
         responseBuilder.setTenantGetUserInfoResponse(getUserInfoResponse);
         break;
+      case ListTenant:
+        ListTenantResponse listTenantResponse = listTenant(
+            request.getListTenantRequest());
+        responseBuilder.setListTenantResponse(listTenantResponse);
+        break;
       default:
         responseBuilder.setSuccess(false);
         responseBuilder.setMessage("Unrecognized Command Type: " + cmdType);
@@ -362,6 +370,7 @@ public class OzoneManagerRequestHandler implements RequestHandler {
 
   private TenantGetUserInfoResponse tenantGetUserInfo(
       TenantGetUserInfoRequest request) throws IOException {
+
     final TenantGetUserInfoResponse.Builder resp =
         TenantGetUserInfoResponse.newBuilder();
     final String userPrincipal = request.getUserPrincipal();
@@ -377,6 +386,18 @@ public class OzoneManagerRequestHandler implements RequestHandler {
     return resp.build();
   }
 
+  private ListTenantResponse listTenant(
+      ListTenantRequest request) throws IOException {
+
+    final ListTenantResponse.Builder resp = ListTenantResponse.newBuilder();
+
+    TenantInfoList ret = impl.listTenant();
+    resp.setSuccess(true);
+    resp.addAllTenantInfo(ret.getTenantInfoList());
+
+    return resp.build();
+  }
+
   private ListVolumeResponse listVolumes(ListVolumeRequest request)
       throws IOException {
     ListVolumeResponse.Builder resp = ListVolumeResponse.newBuilder();
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3GetSecretRequest.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3GetSecretRequest.java
index 0fe87d8..4a47251 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3GetSecretRequest.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3GetSecretRequest.java
@@ -39,14 +39,14 @@ import org.apache.hadoop.ozone.om.request.s3.tenant.OMAssignUserToTenantRequest;
 import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantCreateRequest;
 import org.apache.hadoop.ozone.om.response.OMClientResponse;
 import org.apache.hadoop.ozone.om.response.s3.security.S3GetSecretResponse;
-import org.apache.hadoop.ozone.om.response.s3.tenant.OMAssignUserToTenantResponse;
+import org.apache.hadoop.ozone.om.response.s3.tenant.OMTenantAssignUserAccessIdResponse;
 import org.apache.hadoop.ozone.om.response.s3.tenant.OMTenantCreateResponse;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssignUserToTenantRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CreateTenantRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetS3SecretRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetS3SecretResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Secret;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantAssignUserAccessIdRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
 import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.hadoop.security.authentication.util.KerberosName;
@@ -163,9 +163,9 @@ public class TestS3GetSecretRequest {
 
     return OMRequest.newBuilder()
         .setClientId(UUID.randomUUID().toString())
-        .setCmdType(Type.AssignUserToTenant)
-        .setAssignUserToTenantRequest(
-            AssignUserToTenantRequest.newBuilder()
+        .setCmdType(Type.TenantAssignUserAccessId)
+        .setTenantAssignUserAccessIdRequest(
+            TenantAssignUserAccessIdRequest.newBuilder()
                 .setTenantName(tenantNameStr)
                 .setTenantUsername(userPrincipalStr)
                 .setAccessId(accessIdStr)
@@ -367,17 +367,19 @@ public class TestS3GetSecretRequest {
             txLogIndex, ozoneManagerDoubleBufferHelper);
 
     // Check response type and cast
-    Assert.assertTrue(omClientResponse instanceof OMAssignUserToTenantResponse);
-    final OMAssignUserToTenantResponse omAssignUserToTenantResponse =
-        (OMAssignUserToTenantResponse) omClientResponse;
+    Assert.assertTrue(
+        omClientResponse instanceof OMTenantAssignUserAccessIdResponse);
+    final OMTenantAssignUserAccessIdResponse
+        omTenantAssignUserAccessIdResponse =
+        (OMTenantAssignUserAccessIdResponse) omClientResponse;
 
     // Check response
-    Assert.assertTrue(omAssignUserToTenantResponse.getOMResponse()
+    Assert.assertTrue(omTenantAssignUserAccessIdResponse.getOMResponse()
         .getSuccess());
-    Assert.assertTrue(omAssignUserToTenantResponse.getOMResponse()
-        .getAssignUserToTenantResponse().getSuccess());
+    Assert.assertTrue(omTenantAssignUserAccessIdResponse.getOMResponse()
+        .getTenantAssignUserAccessIdResponse().getSuccess());
     final OmDBAccessIdInfo omDBAccessIdInfo =
-        omAssignUserToTenantResponse.getOmDBAccessIdInfo();
+        omTenantAssignUserAccessIdResponse.getOmDBAccessIdInfo();
     Assert.assertNotNull(omDBAccessIdInfo);
 
 
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/GetUserInfoHandler.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/GetUserInfoHandler.java
index 44f0bda..c3ee7f8 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/GetUserInfoHandler.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/GetUserInfoHandler.java
@@ -69,11 +69,19 @@ public class GetUserInfoHandler extends TenantHandler {
         out().println("User '" + userPrincipal + "' is assigned to:");
 
         for (TenantAccessIdInfo accessIdInfo : accessIdInfoList) {
-          out().println("- Tenant '" + accessIdInfo.getTenantName() +
-              "' with accessId '" + accessIdInfo.getAccessId() + "'");
+          // Get admin info
+          final String adminInfoString;
+          if (accessIdInfo.getIsAdmin()) {
+            adminInfoString = accessIdInfo.getIsDelegatedAdmin() ?
+                " delegated admin" : " admin";
+          } else {
+            adminInfoString = "";
+          }
+          out().format("- Tenant '%s'%s with accessId '%s'%n",
+              accessIdInfo.getTenantName(), adminInfoString,
+              accessIdInfo.getAccessId());
         }
 
-        out().println();
       } catch (IOException e) {
         err().println("Failed to GetUserInfo of user '" + userPrincipal
             + "': " + e.getMessage());
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantAssignAdminHandler.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantAssignAdminHandler.java
new file mode 100644
index 0000000..3e2c659
--- /dev/null
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantAssignAdminHandler.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.hadoop.ozone.shell.tenant;
+
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.shell.OzoneAddress;
+import picocli.CommandLine;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.PERMISSION_DENIED;
+
+/**
+ * ozone tenant user assign-admin.
+ *
+ * User must already be assigned to tenant with, will be rejected otherwise.
+ */
+@CommandLine.Command(name = "assign-admin",
+    aliases = {"assignadmin"},
+    description = "Assign admin role to accessIds in a tenant")
+public class TenantAssignAdminHandler extends TenantHandler {
+
+  @CommandLine.Spec
+  private CommandLine.Model.CommandSpec spec;
+
+  @CommandLine.Parameters(description = "List of accessIds", arity = "1..")
+  private List<String> accessIds = new ArrayList<>();
+
+  @CommandLine.Option(names = {"-t", "--tenant"},
+      description = "Tenant name")
+  private String tenantName;
+
+  @CommandLine.Option(names = {"-d", "--delegated"}, defaultValue = "true",
+      description = "Make delegated admin")
+  private boolean delegated;
+
+  @Override
+  protected void execute(OzoneClient client, OzoneAddress address) {
+    final ObjectStore objStore = client.getObjectStore();
+
+    for (final String accessId : accessIds) {
+      try {
+        objStore.tenantAssignAdmin(accessId, tenantName, delegated);
+        // TODO: Make tenantAssignAdmin return accessId, tenantName, user later.
+        err().println("Assigned admin to '" + accessId +
+            (tenantName != null ? "' in tenant '" + tenantName + "'" : ""));
+      } catch (IOException e) {
+        err().println("Failed to assign admin to '" + accessId +
+            (tenantName != null ? "' in tenant '" + tenantName + "'" : "") +
+            ": " + e.getMessage());
+        if (e instanceof OMException) {
+          final OMException omEx = (OMException) e;
+          // Don't bother continuing the loop if current user isn't Ozone admin
+          if (omEx.getResult().equals(PERMISSION_DENIED)) {
+            break;
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/AssignUserToTenantHandler.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantAssignUserAccessIdHandler.java
similarity index 79%
rename from hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/AssignUserToTenantHandler.java
rename to hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantAssignUserAccessIdHandler.java
index c5feee1..75bf672 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/AssignUserToTenantHandler.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantAssignUserAccessIdHandler.java
@@ -18,7 +18,6 @@
 package org.apache.hadoop.ozone.shell.tenant;
 
 import org.apache.commons.lang3.StringUtils;
-import org.apache.hadoop.hdds.cli.GenericCli;
 import org.apache.hadoop.ozone.client.ObjectStore;
 import org.apache.hadoop.ozone.client.OzoneClient;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
@@ -36,17 +35,18 @@ import static org.apache.hadoop.ozone.OzoneConsts.TENANT_NAME_USER_NAME_DELIMITE
  * ozone tenant user assign.
  */
 @CommandLine.Command(name = "assign",
-    description = "Assign user to tenant")
-public class AssignUserToTenantHandler extends TenantHandler {
+    description = "Assign user accessId to tenant")
+public class TenantAssignUserAccessIdHandler extends TenantHandler {
 
   @CommandLine.Spec
   private CommandLine.Model.CommandSpec spec;
 
-  @CommandLine.Parameters(description = "List of user Kerberos principal(s)")
-  private List<String> principals = new ArrayList<>();
+  @CommandLine.Parameters(description = "List of user principals",
+      arity = "1..")
+  private List<String> userPrincipals = new ArrayList<>();
 
   @CommandLine.Option(names = {"-t", "--tenant"},
-      description = "Tenant name")
+      description = "Tenant name", required = true)
   private String tenantName;
 
   @CommandLine.Option(names = {"-a", "--access-id", "--accessId"},
@@ -59,10 +59,6 @@ public class AssignUserToTenantHandler extends TenantHandler {
   //  `s3 getsecret` and leak the secret if an admin isn't careful.
   private String accessId;
 
-  private boolean isEmptyList(List<String> list) {
-    return list == null || list.size() == 0;
-  }
-
   private String getDefaultAccessId(String principal) {
     return tenantName + TENANT_NAME_USER_NAME_DELIMITER + principal;
   }
@@ -71,33 +67,23 @@ public class AssignUserToTenantHandler extends TenantHandler {
   protected void execute(OzoneClient client, OzoneAddress address) {
     final ObjectStore objStore = client.getObjectStore();
 
-    if (isEmptyList(principals)) {
-      GenericCli.missingSubcommand(spec);
-      return;
-    }
-
-    if (StringUtils.isEmpty(tenantName)) {
-      err().println("Please specify a tenant name with -t.");
-      return;
-    }
-
     if (StringUtils.isEmpty(accessId)) {
-      accessId = getDefaultAccessId(principals.get(0));
-    } else if (principals.size() > 1) {
+      accessId = getDefaultAccessId(userPrincipals.get(0));
+    } else if (userPrincipals.size() > 1) {
       err().println("Manually specifying accessId is only supported when there "
           + "is one user principal in the command line. Reduce the number of "
           + "principal to one and try again.");
       return;
     }
 
-    for (int i = 0; i < principals.size(); i++) {
-      final String principal = principals.get(i);
+    for (int i = 0; i < userPrincipals.size(); i++) {
+      final String principal = userPrincipals.get(i);
       try {
         if (i >= 1) {
           accessId = getDefaultAccessId(principal);
         }
         final S3SecretValue resp =
-            objStore.assignUserToTenant(principal, tenantName, accessId);
+            objStore.tenantAssignUserAccessId(principal, tenantName, accessId);
         err().println("Assigned '" + principal + "' to '" + tenantName +
             "' with accessId '" + accessId + "'.");
         out().println("export AWS_ACCESS_KEY_ID='" +
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantListHandler.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantListHandler.java
new file mode 100644
index 0000000..45f6ccf
--- /dev/null
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantListHandler.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.hadoop.ozone.shell.tenant;
+
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.om.helpers.TenantInfoList;
+import org.apache.hadoop.ozone.shell.OzoneAddress;
+import picocli.CommandLine;
+
+import java.io.IOException;
+
+/**
+ * ozone tenant list.
+ */
+@CommandLine.Command(name = "list",
+    aliases = {"ls"},
+    description = "List tenants")
+public class TenantListHandler extends TenantHandler {
+
+//  @CommandLine.Mixin
+//  private ListOptions listOptions;
+
+//  @CommandLine.Option(names = {"--json", "-j"},
+//      description = "Print the result in JSON.")
+//  private boolean printJson;
+
+  // TODO: long == json later.
+  @CommandLine.Option(names = {"--long"},
+      // Not using -l here as it potentially collides with -l inside ListOptions
+      //  if we do need pagination at some point.
+      description = "List in long format")
+  private boolean longFormat;
+
+  @CommandLine.Option(names = {"--header", "-H"},
+      description = "Print header")
+  private boolean printHeader;
+
+  @Override
+  protected void execute(OzoneClient client, OzoneAddress address) {
+    final ObjectStore objStore = client.getObjectStore();
+    try {
+      TenantInfoList tenantInfoList = objStore.listTenant();
+
+      if (printHeader) {
+        // default console width 80 / 5 = 16. +1 for extra room. Change later?
+        out().format(longFormat ? "%-17s" : "%s%n",
+            "Tenant");
+        if (longFormat) {
+          // TODO: rename these fields?
+          // TODO: print JSON by default after rebase.
+          out().format("%-17s%-17s%-17s%s%n",
+              "BucketNS",
+              "AccountNS",  // == Volume name IIRC ?
+              "UserPolicy",
+              "BucketPolicy");
+        }
+      }
+
+      tenantInfoList.getTenantInfoList().forEach(tenantInfo -> {
+        out().format(longFormat ? "%-17s" : "%s%n",
+            tenantInfo.getTenantName());
+        if (longFormat) {
+          out().format("%-17s%-17s%-17s%s%n",
+              tenantInfo.getBucketNamespaceName(),
+              tenantInfo.getAccountNamespaceName(),
+              tenantInfo.getUserPolicyGroupName(),
+              tenantInfo.getBucketPolicyGroupName());
+        }
+      });
+    } catch (IOException e) {
+      LOG.error("Failed to list tenants: {}", e.getMessage());
+    }
+  }
+}
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantRevokeAdminHandler.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantRevokeAdminHandler.java
new file mode 100644
index 0000000..ad94a9a
--- /dev/null
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantRevokeAdminHandler.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.hadoop.ozone.shell.tenant;
+
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.shell.OzoneAddress;
+import picocli.CommandLine;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.PERMISSION_DENIED;
+
+/**
+ * ozone tenant user revoke-admin.
+ */
+@CommandLine.Command(name = "revoke-admin",
+    aliases = {"revokeadmin"},
+    description = "Revoke admin role from accessIds in a tenant")
+public class TenantRevokeAdminHandler extends TenantHandler {
+
+  @CommandLine.Spec
+  private CommandLine.Model.CommandSpec spec;
+
+  @CommandLine.Parameters(description = "List of accessIds", arity = "1..")
+  private List<String> accessIds = new ArrayList<>();
+
+  @CommandLine.Option(names = {"-t", "--tenant"},
+      description = "Tenant name")
+  private String tenantName;
+
+  @Override
+  protected void execute(OzoneClient client, OzoneAddress address) {
+    final ObjectStore objStore = client.getObjectStore();
+
+    for (final String accessId : accessIds) {
+      try {
+        // TODO: Make tenantRevokeAdmin return accessId, tenantName, user later.
+        objStore.tenantRevokeAdmin(accessId, tenantName);
+        err().println("Revoked admin role of '" + accessId +
+            (tenantName != null ? "' from tenant '" + tenantName + "'" : ""));
+      } catch (IOException e) {
+        err().println("Failed to revoke admin role of '" + accessId +
+            (tenantName != null ? "' from tenant '" + tenantName + "'" : "") +
+            ": " + e.getMessage());
+        if (e instanceof OMException) {
+          final OMException omEx = (OMException) e;
+          // Don't bother continuing the loop if current user isn't Ozone admin
+          if (omEx.getResult().equals(PERMISSION_DENIED)) {
+            break;
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/RevokeUserAccessToTenantHandler.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantRevokeUserAccessIdHandler.java
similarity index 58%
rename from hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/RevokeUserAccessToTenantHandler.java
rename to hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantRevokeUserAccessIdHandler.java
index 0c4cdf7..66f2f8f 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/RevokeUserAccessToTenantHandler.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantRevokeUserAccessIdHandler.java
@@ -17,19 +17,40 @@
  */
 package org.apache.hadoop.ozone.shell.tenant;
 
+import org.apache.hadoop.ozone.client.ObjectStore;
 import org.apache.hadoop.ozone.client.OzoneClient;
 import org.apache.hadoop.ozone.shell.OzoneAddress;
 import picocli.CommandLine;
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * ozone tenant user revoke.
  */
 @CommandLine.Command(name = "revoke",
-    description = "Revoke user access to tenant")
-public class RevokeUserAccessToTenantHandler extends TenantHandler {
+    description = "Revoke user accessId to tenant")
+public class TenantRevokeUserAccessIdHandler extends TenantHandler {
+
+  @CommandLine.Spec
+  private CommandLine.Model.CommandSpec spec;
+
+  @CommandLine.Parameters(description = "List of user accessIds", arity = "1..")
+  private List<String> accessIds = new ArrayList<>();
 
   @Override
   protected void execute(OzoneClient client, OzoneAddress address) {
-    out().println("Not Implemented.");
+    final ObjectStore objStore = client.getObjectStore();
+
+    accessIds.forEach(accessId -> {
+      try {
+        objStore.tenantRevokeUserAccessId(accessId);
+        err().format("Revoked accessId '%s'.%n", accessId);
+      } catch (IOException e) {
+        err().format("Failed to revoke accessId '%s': %s%n",
+            accessId, e.getMessage());
+      }
+    });
   }
 }
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantShell.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantShell.java
index f26e874..3f2c9f7 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantShell.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantShell.java
@@ -32,6 +32,7 @@ import java.util.function.Supplier;
         TenantCreateHandler.class,
         TenantModifyHandler.class,
         TenantDeleteHandler.class,
+        TenantListHandler.class,
         TenantUserCommands.class
     })
 public class TenantShell extends Shell {
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantUserCommands.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantUserCommands.java
index 49c58b5..591d3b0 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantUserCommands.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/shell/tenant/TenantUserCommands.java
@@ -36,8 +36,10 @@ import java.util.concurrent.Callable;
     description = "Tenant user management",
     subcommands = {
         GetUserInfoHandler.class,
-        AssignUserToTenantHandler.class,
-        RevokeUserAccessToTenantHandler.class
+        TenantAssignUserAccessIdHandler.class,
+        TenantRevokeUserAccessIdHandler.class,
+        TenantAssignAdminHandler.class,
+        TenantRevokeAdminHandler.class
     },
     mixinStandardHelpOptions = true,
     versionProvider = HddsVersionProvider.class)

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@ozone.apache.org
For additional commands, e-mail: commits-help@ozone.apache.org