You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@cloudstack.apache.org by bhaisaab <gi...@git.apache.org> on 2016/04/12 19:10:43 UTC

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

GitHub user bhaisaab opened a pull request:

    https://github.com/apache/cloudstack/pull/1489

    CLOUDSTACK-8562: Dynamic Role-Based API Checker for CloudStack

    


You can merge this pull request into a Git repository by running:

    $ git pull https://github.com/shapeblue/cloudstack dynamicroles-master

Alternatively you can review and apply these changes as the patch at:

    https://github.com/apache/cloudstack/pull/1489.patch

To close this pull request, make a commit to your master/trunk branch
with (at least) the following in the commit message:

    This closes #1489
    
----
commit 5e5ee1747f431219e94ef29a9f5c93c1ffc05c34
Author: Rohit Yadav <ro...@shapeblue.com>
Date:   2016-03-25T16:14:11Z

    CLOUDSTACK-8562: Deprecate commands.properties
    
    - Fixes apidocs and marvin to be independent of commands.properties usage
    - Removes bundling of commands.properties in deb/rpm packaging
    - Removes file references across codebase
    
    Signed-off-by: Rohit Yadav <ro...@shapeblue.com>

----


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by anshul1886 <gi...@git.apache.org>.
Github user anshul1886 commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-219987354
  
    @rhtyd Is commands.properties exist in your setup or not? If it exists then things are working fine.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by pdube <gi...@git.apache.org>.
Github user pdube commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59777518
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleType.java ---
    @@ -16,18 +16,90 @@
     // under the License.
     package org.apache.cloudstack.acl;
     
    +import com.cloud.user.Account;
    +import com.google.common.base.Enums;
    +import com.google.common.base.Strings;
    +
     // Enum for default roles in CloudStack
     public enum RoleType {
    -    Admin(1), ResourceAdmin(2), DomainAdmin(4), User(8), Unknown(0);
    +    Admin(1L, Account.ACCOUNT_TYPE_ADMIN, 1),
    +    ResourceAdmin(2L, Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN, 2),
    +    DomainAdmin(3L, Account.ACCOUNT_TYPE_DOMAIN_ADMIN, 4),
    +    User(4L, Account.ACCOUNT_TYPE_NORMAL, 8),
    +    Unknown(-1L, (short) -1, 0);
     
    +    private long id;
    +    private short accountType;
         private int mask;
     
    -    private RoleType(int mask) {
    +    RoleType(final long id, final short accountType, final int mask) {
    +        this.id = id;
    +        this.accountType = accountType;
             this.mask = mask;
         }
     
    -    public int getValue() {
    +    public long getId() {
    +        return id;
    +    }
    +
    +    public short getAccountType() {
    +        return accountType;
    +    }
    +
    +    public int getMask() {
             return mask;
         }
    -}
     
    +    public static RoleType fromString(final String name) {
    +        if (!Strings.isNullOrEmpty(name)
    +                && Enums.getIfPresent(RoleType.class, name).isPresent()) {
    +            return RoleType.valueOf(name);
    +        }
    +        return null;
    +    }
    +
    +    public static RoleType fromMask(int mask) {
    +        for (RoleType roleType : RoleType.values()) {
    +            if (roleType.getMask() == mask) {
    +                return roleType;
    +            }
    +        }
    +        return Unknown;
    +    }
    +
    +    public static RoleType getByAccountType(final short accountType) {
    +        RoleType roleType = RoleType.Unknown;
    +        switch (accountType) {
    +            case Account.ACCOUNT_TYPE_ADMIN:
    +                roleType = RoleType.Admin;
    +                break;
    +            case Account.ACCOUNT_TYPE_DOMAIN_ADMIN:
    +                roleType = RoleType.DomainAdmin;
    +                break;
    +            case Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN:
    +                roleType = RoleType.ResourceAdmin;
    --- End diff --
    
    I meant the ResourceAdmin specifically. What is this role? Is there any documentation on it?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-209856388
  
    thanks @DaanHoogland 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220397187
  
    @rhtyd thanks, just wanted to make sure we extracted anything that was useful from @anshul1886's experience.  Thanks for working this out guys...


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-209028162
  
    @bhaisaab, ok will continue tomorrow.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59788566
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/DeleteRolePermissionCmd.java ---
    @@ -0,0 +1,84 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.user.Account;
    +import org.apache.cloudstack.acl.RolePermission;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.RolePermissionResponse;
    +import org.apache.cloudstack.api.response.SuccessResponse;
    +import org.apache.cloudstack.context.CallContext;
    +
    +@APICommand(name = DeleteRolePermissionCmd.APINAME, description = "Deletes a role permission", responseObject = SuccessResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class DeleteRolePermissionCmd extends BaseCmd {
    +    public static final String APINAME = "deleteRolePermission";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.ID, type = BaseCmd.CommandType.UUID, required = true, entityType = RolePermissionResponse.class, description = "ID of the role permission")
    +    private Long rolePermissionId;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public Long getRolePermissionId() {
    +        return rolePermissionId;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() {
    +        if (getRolePermissionId() == null || getRolePermissionId() < 1L) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role permission id provided");
    +        }
    +        RolePermission rolePermission = roleService.findRolePermission(getRolePermissionId());
    +        if (rolePermission == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role permission id provided");
    +        }
    --- End diff --
    
    Consider extracting this validation logic to a private method to make things more readable.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59514329
  
    --- Diff: api/test/org/apache/cloudstack/acl/RuleTest.java ---
    @@ -0,0 +1,39 @@
    +package org.apache.cloudstack.acl;
    --- End diff --
    
    license missing


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-213328879
  
    @jburwell @swill @DaanHoogland @pdube @koushik-das @pdube please share your LGTM if you're satisfied with the changes, or please comment what else needs fixing. Thanks.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59852554
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/CreateRoleCmd.java ---
    @@ -0,0 +1,104 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.user.Account;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +import org.apache.cloudstack.context.CallContext;
    +
    +@APICommand(name = CreateRoleCmd.APINAME, description = "Creates a role", responseObject = RoleResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class CreateRoleCmd extends BaseCmd {
    +    public static final String APINAME = "createRole";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "creates a role with this unique name")
    +    private String roleName;
    +
    +    @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "The type of the role, valid options are: Admin, ResourceAdmin, DomainAdmin, User")
    +    private String roleType;
    +
    +    @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the role")
    +    private String roleDescription;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public String getRoleName() {
    +        return roleName;
    +    }
    +
    +    public RoleType getRoleType() {
    +        return RoleType.fromString(roleType);
    +    }
    +
    +    public String getRoleDescription() {
    +        return roleDescription;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() {
    +        if (Strings.isNullOrEmpty(getRoleName())) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty role name provided");
    +        }
    +        if (getRoleType() == null || getRoleType() == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
    +        }
    +        CallContext.current().setEventDetails("Role: " + getRoleName() + ", type:" + getRoleType() + ", description: " + getRoleDescription());
    +        Role role = roleService.createRole(getRoleName(), getRoleType(), getRoleDescription());
    +        if (role == null) {
    +            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create role");
    +        }
    +        RoleResponse response = new RoleResponse();
    +        response.setId(role.getUuid());
    +        response.setRoleName(role.getName());
    +        response.setRoleType(role.getRoleType());
    +        response.setResponseName(getCommandName());
    +        response.setObjectName("role");
    +        setResponseObject(response);
    +    }
    --- End diff --
    
    Fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by anshul1886 <gi...@git.apache.org>.
Github user anshul1886 commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220227876
  
    @rhtyd Its a clean setup. It is not even complete because of one other bug introduced by PR #816.
    
    Summary of the steps:
    
    1. mvn -P systemvm clean install  -DskipTests
    2. mvn -P developer -pl developer -Ddeploydb
    3. mvn -pl :cloud-client-ui jetty:run
    4. Open UI start seeing those errors.
    
    For commit history on my setup see PR #351.
    
    And that commits code is not even coming into picture.
    
    To go ahead I have disabled dynamic.apichecker.enabled and copied the commands.properties file. After that I was blocked by the bug introduced by #816. So I have not completed my setup. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59515911
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/CreateRolePermissionCmd.java ---
    @@ -0,0 +1,121 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.user.Account;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RolePermission;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.acl.Rule;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.RolePermissionResponse;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +import org.apache.cloudstack.context.CallContext;
    +
    +@APICommand(name = CreateRolePermissionCmd.APINAME, description = "Adds a API permission to a role", responseObject = RolePermissionResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class CreateRolePermissionCmd extends BaseCmd {
    +    public static final String APINAME = "createRolePermission";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.ROLE_ID, type = CommandType.UUID, required = true, entityType = RoleResponse.class, description = "ID of the role")
    +    private Long roleId;
    +
    +    @Parameter(name = ApiConstants.RULE, type = CommandType.STRING, required = true, description = "The API name or wildcard rule such as list*")
    +    private String rule;
    +
    +    @Parameter(name = ApiConstants.PERMISSION, type = CommandType.STRING, required = true, description = "The rule permission, allow or deny. Default: deny.")
    +    private String permission;
    +
    +    @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the role permission")
    +    private String description;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public Long getRoleId() {
    +        return roleId;
    +    }
    +
    +    public Rule getRule() {
    +        return new Rule(rule);
    +    }
    +
    +    public RolePermission.Permission getPermission() {
    +        if (Strings.isNullOrEmpty(permission)) {
    +            return null;
    +        }
    +        return RolePermission.Permission.valueOf(permission.toUpperCase());
    +    }
    +
    +    public String getDescription() {
    +        return description;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() {
    +        if (getRule() == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role permission rule provided");
    +        }
    +        Role role = roleService.findRole(getRoleId());
    +        if (role == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    +        CallContext.current().setEventDetails("Role id: " + role.getId() + ", rule:" + getRule() + ", permission: " + getPermission() + ", description: " + getDescription());
    +        RolePermission rolePermission = roleService.createRolePermission(role, getRule(), getPermission(), getDescription());
    +        if (rolePermission == null) {
    +            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create role permission");
    +        }
    +
    +        RolePermissionResponse response = new RolePermissionResponse();
    +        response.setId(rolePermission.getUuid());
    +        response.setRoleId(role.getUuid());
    +        response.setRule(rolePermission.getRule());
    +        response.setRulePermission(rolePermission.getPermission());
    +        response.setDescription(rolePermission.getDescription());
    +        response.setResponseName(getCommandName());
    +        response.setObjectName("rolepermission");
    +        setResponseObject(response);
    +     }
    --- End diff --
    
    same as CreateRoleCmd; maybe factor this block out for readability?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59857962
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    --- End diff --
    
    fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-212063057
  
    @swill I looked at it, looks like an environment issue? I was able to build it without issue (also did not find 'root_admin' key in the code. I've rebased master and doing a push -f. Travis builds apidocs as well, we'll know if it fails/passes.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59524315
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleService.java ---
    @@ -0,0 +1,43 @@
    +// 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.cloudstack.acl;
    +
    +import org.apache.cloudstack.framework.config.ConfigKey;
    +
    +import java.util.List;
    +
    +public interface RoleService {
    +
    +    ConfigKey<Boolean> EnableDynamicApiChecker = new ConfigKey<>("Hidden", Boolean.class, "dynamic.apichecker.enabled", "false",
    +            "If set to true, this enables the dynamic role-based api access checker and disables the default static role-based api access checker.",
    +            true);
    +
    +    boolean isEnabled();
    +    Role findRole(final Long id);
    +    Role createRole(final String name, final RoleType roleType, final String description);
    +    boolean updateRole(final Role role, final String name, final RoleType roleType, final String description);
    +    boolean deleteRole(final Role role);
    +
    +    RolePermission findRolePermission(final Long id);
    +    RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description);
    +    boolean updateRolePermission(final RolePermission rolePermission, final Rule rule, final RolePermission.Permission permission, final String description);
    +    boolean deleteRolePermission(final RolePermission rolePermission);
    +
    +    List<Role> findAllRolesBy(final Long id, final String name, final RoleType roleType);
    --- End diff --
    
    @bhaisaab, you could split them or add a javadoc so users get a clue. If the caller is in control splitting makes sense. They'd know what they are passing anyway.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60433227
  
    --- Diff: api/src/org/apache/cloudstack/acl/Rule.java ---
    @@ -0,0 +1,65 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.google.common.base.Strings;
    +
    +import java.util.regex.Pattern;
    +
    +public final class Rule {
    +    private final String rule;
    +    private final static Pattern ALLOWED_PATTERN = Pattern.compile("^[a-zA-Z0-9*]+$");
    +
    +    public Rule(final String rule) {
    +        validate(rule);
    +        this.rule = rule;
    +    }
    +
    +    public boolean matches(final String commandName) {
    +        if (Strings.isNullOrEmpty(commandName)) {
    +            return false;
    +        }
    +        if (isWildcard()) {
    +            if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                return true;
    +            }
    +        } else {
    +            if (commandName.equalsIgnoreCase(rule)) {
    +                return true;
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isWildcard() {
    +        return rule.contains("*");
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return rule;
    +    }
    +
    +    private static boolean validate(final String rule) throws InvalidParameterValueException {
    +        if (Strings.isNullOrEmpty(rule) || !ALLOWED_PATTERN.matcher(rule).matches()) {
    +            throw new InvalidParameterValueException("Invalid rule provided. Only API names and wildcards are allowed.");
    --- End diff --
    
    Add the rule value into the error message for debugging.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-209018498
  
    ## test_staticroles.py
    
    === TestName: test_static_role_account_acls | Status : SUCCESS ===
    
    ## scripts/util/migrate-dynamicroles.py
    
    I'll add docs for this feature in a separate PR to admin-docs, this is the usage information:
    
    Usage: migrate-dynamicroles.py [options]
    
    Options:
      -h, --help            show this help message and exit
      -b DB, --db=DB        The name of the database, default: cloud
      -u USER, --user=USER  User name a MySQL user with privileges on cloud
                            database
      -p PASSWORD, --password=PASSWORD
                            Password of a MySQL user with privileges on cloud
                            database
      -H HOST, --host=HOST  Host or IP of the MySQL server
      -P PORT, --port=PORT  Host or IP of the MySQL server
      -f COMMANDSFILE, --properties-file=COMMANDSFILE
                            The commands.properties file
      -d, --dryrun          Dry run and debug operations this tool will perform
    
    $ sudo ./scripts/util/migrate-dynamicroles.py
    Apache CloudStack Role Permission Migration Tool
    (c) Apache CloudStack Authors and the ASF, under the Apache License, Version 2.0
    
    Running this migration tool will remove any default-role rules in cloud.role_permissions. Do you want to continue? [y/N]y
    The commands.properties file has been deprecated and moved at: /etc/cloudstack/management/commands.properties.deprecated
    Static role permissions from commands.properties have been migrated into the db
    Dynamic role based API checker has been enabled!
    
    ## test_dynamicroles.py
    
    === TestName: test_default_role_deletion | Status : SUCCESS ===
    
    === TestName: test_role_account_acls | Status : SUCCESS ===
    
    === TestName: test_role_account_acls_multiple_mgmt_servers | Status : SUCCESS ===
    
    === TestName: test_role_inuse_deletion | Status : SUCCESS ===
    
    === TestName: test_role_lifecycle_create | Status : SUCCESS ===
    
    === TestName: test_role_lifecycle_delete | Status : SUCCESS ===
    
    === TestName: test_role_lifecycle_list | Status : SUCCESS ===
    
    === TestName: test_role_lifecycle_update | Status : SUCCESS ===
    
    === TestName: test_role_lifecycle_update_role_inuse | Status : SUCCESS ===
    
    === TestName: test_rolepermission_lifecycle_create | Status : SUCCESS ===
    
    === TestName: test_rolepermission_lifecycle_delete | Status : SUCCESS ===
    
    === TestName: test_rolepermission_lifecycle_list | Status : SUCCESS ===
    
    === TestName: test_rolepermission_lifecycle_update | Status : SUCCESS ===
    
    === TestName: test_rolepermission_lifecycle_update_invalid_rule | Status : SUCCESS ===


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215889804
  
    @swill fixed the issue of orderable permissions, PR is ready for testing/merge


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-209021645
  
    This is a feature that we definitely need.  Can I please get some code review on this PR?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214728007
  
    @rhtyd I think you still didn't get my point. Read the comment again. I would like to see the option for using command.properties available for both new and old installations until the user explicitly wants to migrate. Don't remove the file as you have done in the PR.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-211374296
  
    Nice work on the quality code reviews guys.  @DaanHoogland since there have been a bunch of changes, would you mind running your CI against this one?  I am sorting out some issues in my env right now cause I had to change my hardware.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-209026461
  
    @swill @bhaisaab I started reviewing but it changed while busy, Is it done now?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220389940
  
    @swill the problem was environment dependent where an unclean environment was used to build/run management server, it is recommended to git clean -fdx one's local repository (or at least run a mvn clean -P developer,systemvm -Dnoredist -Dsimulator) to get rid of old files.
    
    The case has been documented on the FS that for `dynamic roles feature to work the setting value should be true and there should be no commands.properties file readable on the classpath`.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by pdube <gi...@git.apache.org>.
Github user pdube commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59730927
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleService.java ---
    @@ -0,0 +1,43 @@
    +// 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.cloudstack.acl;
    +
    +import org.apache.cloudstack.framework.config.ConfigKey;
    +
    +import java.util.List;
    +
    +public interface RoleService {
    +
    +    ConfigKey<Boolean> EnableDynamicApiChecker = new ConfigKey<>("Hidden", Boolean.class, "dynamic.apichecker.enabled", "false",
    +            "If set to true, this enables the dynamic role-based api access checker and disables the default static role-based api access checker.",
    +            true);
    +
    +    boolean isEnabled();
    +    Role findRole(final Long id);
    +    Role createRole(final String name, final RoleType roleType, final String description);
    +    boolean updateRole(final Role role, final String name, final RoleType roleType, final String description);
    +    boolean deleteRole(final Role role);
    +
    +    RolePermission findRolePermission(final Long id);
    +    RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description);
    +    boolean updateRolePermission(final RolePermission rolePermission, final Rule rule, final RolePermission.Permission permission, final String description);
    +    boolean deleteRolePermission(final RolePermission rolePermission);
    +
    +    List<Role> findAllRolesBy(final Long id, final String name, final RoleType roleType);
    --- End diff --
    
    I would have to agree with @DaanHoogland. Splitting into separate methods would be clearer and probably simplify the business logic per method as well (so it would be simpler to unit test).


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60745471
  
    --- Diff: utils/src/main/java/com/cloud/utils/PropertiesUtil.java ---
    @@ -34,6 +34,10 @@
     public class PropertiesUtil {
         private static final Logger s_logger = Logger.getLogger(PropertiesUtil.class);
     
    +    public static String getDefaultApiCommandsFileName() {
    +        return "commands.properties";
    +    }
    --- End diff --
    
    Why is this method not defined in a common class/interface of the authenticator mechanism?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60771659
  
    --- Diff: scripts/util/migrate-dynamicroles.py ---
    @@ -0,0 +1,134 @@
    +#!/usr/bin/python
    +# -*- coding: utf-8 -*-
    +# 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.
    +
    +import os
    +import sys
    +import uuid
    +
    +from contextlib import closing
    +from optparse import OptionParser
    +
    +try:
    +    import MySQLdb
    +except ImportError:
    +    print("MySQLdb cannot be imported, please install python-mysqldb(apt) or mysql-python(yum)")
    +    sys.exit(1)
    +
    +dryrun = False
    +
    +
    +def runSql(conn, query):
    +    if dryrun:
    +        print("Running SQL query: " + query)
    +        return
    +    with closing(conn.cursor()) as cursor:
    +        cursor.execute(query)
    +
    +
    +def migrateApiRolePermissions(apis, conn):
    +    # All allow for root admin role Admin(id:1)
    +    runSql(conn, "INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), 1, '*', 'Allow');")
    +    # Migrate rules based on commands.properties rule for ResourceAdmin(id:2), DomainAdmin(id:3), User(id:4)
    +    octetKey = {2:2, 3:4, 4:8}
    +    for role in [2, 3, 4]:
    +        for api in sorted(apis.keys()):
    +            # Ignore auth commands
    +            if api in ['login', 'logout', 'samlSso', 'samlSlo', 'listIdps', 'listAndSwitchSamlAccount', 'getSPMetadata']:
    +                continue
    +            if (octetKey[role] & int(apis[api])) > 0:
    +                runSql(conn, "INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'Allow');" % (role, api))
    +
    +
    +def main():
    +    parser = OptionParser()
    +    parser.add_option("-b", "--db", action="store", type="string", dest="db", default="cloud",
    +                        help="The name of the database, default: cloud")
    +    parser.add_option("-u", "--user", action="store", type="string", dest="user", default="cloud",
    +                        help="User name a MySQL user with privileges on cloud database")
    +    parser.add_option("-p", "--password", action="store", type="string", dest="password", default="cloud",
    +                        help="Password of a MySQL user with privileges on cloud database")
    +    parser.add_option("-H", "--host", action="store", type="string", dest="host", default="127.0.0.1",
    +                        help="Host or IP of the MySQL server")
    +    parser.add_option("-P", "--port", action="store", type="int", dest="port", default=3306,
    +                        help="Host or IP of the MySQL server")
    +    parser.add_option("-f", "--properties-file", action="store", type="string", dest="commandsfile", default="/etc/cloudstack/management/commands.properties",
    +                        help="The commands.properties file")
    +    parser.add_option("-d", "--dryrun", action="store_true", dest="dryrun", default=False,
    +                        help="Dry run and debug operations this tool will perform")
    +    (options, args) = parser.parse_args()
    +
    +    print("Apache CloudStack Role Permission Migration Tool")
    +    print("(c) Apache CloudStack Authors and the ASF, under the Apache License, Version 2.0\n")
    +
    +    global dryrun
    +    if options.dryrun:
    +        dryrun = True
    +
    +    conn = MySQLdb.connect(
    +            host=options.host,
    +            user=options.user,
    +            passwd=options.password,
    +            port=int(options.port),
    +            db=options.db)
    +
    +    if not os.path.isfile(options.commandsfile):
    +        print("Provided commands.properties cannot be accessed or does not exist, please check check permissions")
    +        sys.exit(1)
    +
    +    while True:
    +        choice = raw_input("Running this migration tool will remove any " +
    +                           "default-role rules in cloud.role_permissions. " +
    +                           "Do you want to continue? [y/N]").lower()
    --- End diff --
    
    this upgrade script would only remove rules defined for the default roles only; so it would be good to have that but this won't remove rules defined for any custom/new role. @jburwell it was easier to avoid checking that, and assume admin knows what they are doing with running this script (i.e. upgrading to dynamic roles), pl advise if we should do the additional check?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60772677
  
    --- Diff: utils/src/main/java/com/cloud/utils/PropertiesUtil.java ---
    @@ -34,6 +34,10 @@
     public class PropertiesUtil {
         private static final Logger s_logger = Logger.getLogger(PropertiesUtil.class);
     
    +    public static String getDefaultApiCommandsFileName() {
    +        return "commands.properties";
    +    }
    --- End diff --
    
    PropertiesUtil deals with commands.properties file related processing, so I've put it here. Also, utils is a common packages used by several other modules, putting this in an authenticator (server or plugin package) won't make it generally consumable across several modules if needed.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60399552
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            final String rule = permission.getRule();
    +            if (rule.contains("*")) {
    +                if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                    return true;
    +                }
    +            } else {
    +                if (commandName.equals(rule)) {
    +                    return true;
    +                }
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    --- End diff --
    
    From my reading of the code APIParam is not used in the static role checker. It only looks for authorized in APICommand annotation which again is not getting used in any existing APIs.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60355293
  
    --- Diff: engine/schema/src/org/apache/cloudstack/acl/RolePermissionVO.java ---
    @@ -0,0 +1,109 @@
    +// 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.cloudstack.acl;
    +
    +import javax.persistence.Column;
    +import javax.persistence.Entity;
    +import javax.persistence.EnumType;
    +import javax.persistence.Enumerated;
    +import javax.persistence.GeneratedValue;
    +import javax.persistence.GenerationType;
    +import javax.persistence.Id;
    +import javax.persistence.Table;
    +import java.util.UUID;
    +
    +@Entity
    +@Table(name = "role_permissions")
    +public class RolePermissionVO implements RolePermission {
    --- End diff --
    
    @bhaisaab Based on the code, Role contains one or more RolePermissions. With this approach if a new API gets added what is the effort to update the roles and permissions? Would it be better to instead use Role refers RolePermissions to avoid duplication of permissions?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60938011
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,167 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.log4j.Logger;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    protected static final Logger LOGGER = Logger.getLogger(DynamicRoleBasedAPIAccessChecker.class);
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null || permissions.isEmpty() || Strings.isNullOrEmpty(commandName)) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            try {
    +                if (permission.getRule().matches(commandName)) {
    +                    return true;
    +                }
    +            } catch (InvalidParameterValueException e) {
    +                LOGGER.warn("Invalid rule permission, please fix id=" + permission.getId() + " rule=" + permission.getRule());
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    +            return true;
    +        }
    +
    +        // Check for deny rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.DENY, commandName)) {
    --- End diff --
    
    @koushik-das (2) the `checkPermission` method is done in memory is fast enough for each of ALLOW and DENY permission type on the same list of role permissions


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-212064683
  
    I was confused by this as well.  I will kick off another CI run against this PR and see if I have the problem again.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60937993
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,167 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.log4j.Logger;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    protected static final Logger LOGGER = Logger.getLogger(DynamicRoleBasedAPIAccessChecker.class);
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null || permissions.isEmpty() || Strings.isNullOrEmpty(commandName)) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            try {
    +                if (permission.getRule().matches(commandName)) {
    +                    return true;
    +                }
    +            } catch (InvalidParameterValueException e) {
    +                LOGGER.warn("Invalid rule permission, please fix id=" + permission.getId() + " rule=" + permission.getRule());
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    --- End diff --
    
    @koushik-das (1) See ^^ there is one DB call to get all permissions, not two.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60439393
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            final String rule = permission.getRule();
    +            if (rule.contains("*")) {
    +                if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                    return true;
    +                }
    +            } else {
    +                if (commandName.equals(rule)) {
    +                    return true;
    +                }
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    --- End diff --
    
    @koushik-das all apis are injected into apichecker classes, and at configure time it creates an internal map of all apis (see the code around bottom half of the dynamic api checker class); this way we get the authorized annotation; I'll tag you on the respective code for you to follow


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214960750
  
    @mlsorensen +1 -- well said.  command.properties presents a number of significant problems.  First, in a clustered environment, it is possible (and quite easy) for there to be inconsistent security policies across the cluster.  Second, command.properties does not permit the definition of new roles.  Limiting users to 4 roles in a modern cloud environment is a barrier to CloudStack adoption.
    
    As Marcus has stated, we can't move forward if we keep trying to maintain outdated mechanisms.  Existing users can continue to use command.properties and migrate if/when they desire.  New users gain the benefits of a more reliable and flexible authorization implementation.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216435191
  
    @swill yes indeed we've performed upgrade tests with 4.8.1 to 4.9.0-SNAPSHOT (not using the feature by default) and then using migrate-dyanamicroles.py script; the other test we performed was fresh installation of 4.9.0-SNAPSHOT packages @borisstoyanov can update in more detail


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60383218
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    --- End diff --
    
    This method would be called for all API calls in case dynamic access checker is enabled. So as long as looping is not a bottleneck its ok. If the use-case require only a few permissions per role then you may leave it for now.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60743595
  
    --- Diff: server/src/org/apache/cloudstack/acl/RoleManagerImpl.java ---
    @@ -0,0 +1,273 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.event.ActionEvent;
    +import com.cloud.event.EventTypes;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.dao.AccountDao;
    +import com.cloud.utils.PropertiesUtil;
    +import com.cloud.utils.component.ManagerBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.cloud.utils.db.Transaction;
    +import com.cloud.utils.db.TransactionCallback;
    +import com.cloud.utils.db.TransactionStatus;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.dao.RoleDao;
    +import org.apache.cloudstack.acl.dao.RolePermissionsDao;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.command.admin.acl.CreateRoleCmd;
    +import org.apache.cloudstack.api.command.admin.acl.CreateRolePermissionCmd;
    +import org.apache.cloudstack.api.command.admin.acl.DeleteRoleCmd;
    +import org.apache.cloudstack.api.command.admin.acl.DeleteRolePermissionCmd;
    +import org.apache.cloudstack.api.command.admin.acl.ListRolePermissionsCmd;
    +import org.apache.cloudstack.api.command.admin.acl.ListRolesCmd;
    +import org.apache.cloudstack.api.command.admin.acl.UpdateRoleCmd;
    +import org.apache.cloudstack.api.command.admin.acl.UpdateRolePermissionCmd;
    +import org.apache.cloudstack.context.CallContext;
    +import org.apache.cloudstack.framework.config.ConfigKey;
    +import org.apache.cloudstack.framework.config.Configurable;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import java.io.File;
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +@Local(value = {RoleService.class})
    +public class RoleManagerImpl extends ManagerBase implements RoleService, Configurable, PluggableService {
    +    @Inject
    +    private AccountDao accountDao;
    +    @Inject
    +    private RoleDao roleDao;
    +    @Inject
    +    private RolePermissionsDao rolePermissionsDao;
    +
    +    private void checkCallerAccess() {
    +        if (!isEnabled()) {
    +            throw new PermissionDeniedException("Dynamic api checker is not enabled, aborting role operation");
    +        }
    +        Account caller = CallContext.current().getCallingAccount();
    +        if (caller == null || caller.getRoleId() == null) {
    +            throw new PermissionDeniedException("Restricted API called by an invalid user account");
    +        }
    +        Role callerRole = findRole(caller.getRoleId());
    +        if (callerRole == null || callerRole.getRoleType() != RoleType.Admin) {
    +            throw new PermissionDeniedException("Restricted API called by an user account of non-Admin role type");
    +        }
    +    }
    +
    +    @Override
    +    public boolean isEnabled() {
    +        File apiCmdFile = PropertiesUtil.findConfigFile(PropertiesUtil.getDefaultApiCommandsFileName());
    +        return RoleService.EnableDynamicApiChecker.value() && (apiCmdFile == null || !apiCmdFile.exists());
    +    }
    +
    +    @Override
    +    public Role findRole(final Long id) {
    +        if (id == null || id < 1L) {
    +            return null;
    +        }
    +        return roleDao.findById(id);
    +    }
    +
    +    @Override
    +    public RolePermission findRolePermission(final Long id) {
    +        if (id == null) {
    +            return null;
    +        }
    +        return rolePermissionsDao.findById(id);
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_CREATE, eventDescription = "creating Role")
    +    public Role createRole(final String name, final RoleType roleType, final String description) {
    +        checkCallerAccess();
    +        if (roleType == null || roleType == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
    +        }
    +        return Transaction.execute(new TransactionCallback<RoleVO>() {
    +            @Override
    +            public RoleVO doInTransaction(TransactionStatus status) {
    +                return roleDao.persist(new RoleVO(name, roleType, description));
    +            }
    +        });
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_UPDATE, eventDescription = "updating Role")
    +    public boolean updateRole(final Role role, final String name, final RoleType roleType, final String description) {
    +        checkCallerAccess();
    +        if (role == null) {
    +            return false;
    +        }
    +        if (roleType != null && roleType == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unknown is not a valid role type");
    +        }
    +        RoleVO roleVO = (RoleVO) role;
    +        if (!Strings.isNullOrEmpty(name)) {
    +            roleVO.setName(name);
    +        }
    +        if (roleType != null) {
    +            if (role.getId() <= RoleType.User.getId()) {
    +                throw new PermissionDeniedException("The role type of default roles cannot be changed");
    +            }
    +            List<? extends Account> accounts = accountDao.findAccountsByRole(role.getId());
    +            if (accounts == null || accounts.isEmpty()) {
    +                roleVO.setRoleType(roleType);
    +            } else {
    +                throw new PermissionDeniedException("Found accounts that have role in use, won't allow to change role type");
    +            }
    +        }
    +        if (!Strings.isNullOrEmpty(description)) {
    +            roleVO.setDescription(description);
    +        }
    +        return roleDao.update(role.getId(), roleVO);
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_DELETE, eventDescription = "deleting Role")
    +    public boolean deleteRole(final Role role) {
    +        checkCallerAccess();
    +        if (role == null) {
    +            return false;
    +        }
    +        if (role.getId() <= RoleType.User.getId()) {
    +            throw new PermissionDeniedException("Default roles cannot be deleted");
    +        }
    +        List<? extends Account> accounts = accountDao.findAccountsByRole(role.getId());
    +        if (accounts == null || accounts.size() == 0) {
    +            return Transaction.execute(new TransactionCallback<Boolean>() {
    +                @Override
    +                public Boolean doInTransaction(TransactionStatus status) {
    +                    List<? extends RolePermission> rolePermissions = rolePermissionsDao.findAllByRoleId(role.getId());
    +                    if (rolePermissions != null && !rolePermissions.isEmpty()) {
    +                        for (RolePermission rolePermission : rolePermissions) {
    +                            rolePermissionsDao.remove(rolePermission.getId());
    +                        }
    +                    }
    +                    return roleDao.remove(role.getId());
    +                }
    +            });
    +        }
    +        throw new PermissionDeniedException("Found accounts that have role in use, won't allow to delete role");
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_CREATE, eventDescription = "creating Role Permission")
    +    public RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description) {
    +        checkCallerAccess();
    +        return Transaction.execute(new TransactionCallback<RolePermissionVO>() {
    +            @Override
    +            public RolePermissionVO doInTransaction(TransactionStatus status) {
    +                return rolePermissionsDao.persist(new RolePermissionVO(role.getId(), rule.toString(), permission, description));
    +            }
    +        });
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_UPDATE, eventDescription = "updating Role Permission")
    +    public boolean updateRolePermission(final RolePermission rolePermission, final Rule rule, final RolePermission.Permission permission, final String description) {
    +        checkCallerAccess();
    +        if (rolePermission == null) {
    +            return false;
    +        }
    +        RolePermissionVO rolePermissionVO = (RolePermissionVO) rolePermission;
    +        if (rule != null) {
    +            rolePermissionVO.setRule(rule.toString());
    +        }
    +        if (permission != null) {
    +            rolePermissionVO.setPermission(permission);
    +        }
    +        if (!Strings.isNullOrEmpty(description)) {
    +            rolePermissionVO.setDescription(description);
    +        }
    +        return rolePermissionsDao.update(rolePermission.getId(), rolePermissionVO);
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_DELETE, eventDescription = "deleting Role Permission")
    +    public boolean deleteRolePermission(final RolePermission rolePermission) {
    +        checkCallerAccess();
    +        return rolePermission != null && rolePermissionsDao.remove(rolePermission.getId());
    +    }
    +
    +    private List<Role> toRoleList(final List<? extends Role> roles) {
    +        if (roles != null) {
    +            return new ArrayList<>(roles);
    +        }
    +        return new ArrayList<>();
    --- End diff --
    
    Minor nit: ``Collection.emptyList()`` avoids unnecessary object creation in the empty case.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60881018
  
    --- Diff: server/src/org/apache/cloudstack/acl/RoleManagerImpl.java ---
    @@ -0,0 +1,273 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.event.ActionEvent;
    +import com.cloud.event.EventTypes;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.dao.AccountDao;
    +import com.cloud.utils.PropertiesUtil;
    +import com.cloud.utils.component.ManagerBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.cloud.utils.db.Transaction;
    +import com.cloud.utils.db.TransactionCallback;
    +import com.cloud.utils.db.TransactionStatus;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.dao.RoleDao;
    +import org.apache.cloudstack.acl.dao.RolePermissionsDao;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.command.admin.acl.CreateRoleCmd;
    +import org.apache.cloudstack.api.command.admin.acl.CreateRolePermissionCmd;
    +import org.apache.cloudstack.api.command.admin.acl.DeleteRoleCmd;
    +import org.apache.cloudstack.api.command.admin.acl.DeleteRolePermissionCmd;
    +import org.apache.cloudstack.api.command.admin.acl.ListRolePermissionsCmd;
    +import org.apache.cloudstack.api.command.admin.acl.ListRolesCmd;
    +import org.apache.cloudstack.api.command.admin.acl.UpdateRoleCmd;
    +import org.apache.cloudstack.api.command.admin.acl.UpdateRolePermissionCmd;
    +import org.apache.cloudstack.context.CallContext;
    +import org.apache.cloudstack.framework.config.ConfigKey;
    +import org.apache.cloudstack.framework.config.Configurable;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import java.io.File;
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +@Local(value = {RoleService.class})
    +public class RoleManagerImpl extends ManagerBase implements RoleService, Configurable, PluggableService {
    +    @Inject
    +    private AccountDao accountDao;
    +    @Inject
    +    private RoleDao roleDao;
    +    @Inject
    +    private RolePermissionsDao rolePermissionsDao;
    +
    +    private void checkCallerAccess() {
    +        if (!isEnabled()) {
    +            throw new PermissionDeniedException("Dynamic api checker is not enabled, aborting role operation");
    +        }
    +        Account caller = CallContext.current().getCallingAccount();
    +        if (caller == null || caller.getRoleId() == null) {
    +            throw new PermissionDeniedException("Restricted API called by an invalid user account");
    +        }
    +        Role callerRole = findRole(caller.getRoleId());
    +        if (callerRole == null || callerRole.getRoleType() != RoleType.Admin) {
    +            throw new PermissionDeniedException("Restricted API called by an user account of non-Admin role type");
    +        }
    +    }
    +
    +    @Override
    +    public boolean isEnabled() {
    +        File apiCmdFile = PropertiesUtil.findConfigFile(PropertiesUtil.getDefaultApiCommandsFileName());
    +        return RoleService.EnableDynamicApiChecker.value() && (apiCmdFile == null || !apiCmdFile.exists());
    +    }
    +
    +    @Override
    +    public Role findRole(final Long id) {
    +        if (id == null || id < 1L) {
    +            return null;
    +        }
    +        return roleDao.findById(id);
    +    }
    +
    +    @Override
    +    public RolePermission findRolePermission(final Long id) {
    +        if (id == null) {
    +            return null;
    +        }
    +        return rolePermissionsDao.findById(id);
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_CREATE, eventDescription = "creating Role")
    +    public Role createRole(final String name, final RoleType roleType, final String description) {
    +        checkCallerAccess();
    +        if (roleType == null || roleType == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
    +        }
    +        return Transaction.execute(new TransactionCallback<RoleVO>() {
    +            @Override
    +            public RoleVO doInTransaction(TransactionStatus status) {
    +                return roleDao.persist(new RoleVO(name, roleType, description));
    +            }
    +        });
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_UPDATE, eventDescription = "updating Role")
    +    public boolean updateRole(final Role role, final String name, final RoleType roleType, final String description) {
    +        checkCallerAccess();
    +        if (role == null) {
    +            return false;
    +        }
    +        if (roleType != null && roleType == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unknown is not a valid role type");
    +        }
    +        RoleVO roleVO = (RoleVO) role;
    +        if (!Strings.isNullOrEmpty(name)) {
    +            roleVO.setName(name);
    +        }
    +        if (roleType != null) {
    +            if (role.getId() <= RoleType.User.getId()) {
    +                throw new PermissionDeniedException("The role type of default roles cannot be changed");
    +            }
    +            List<? extends Account> accounts = accountDao.findAccountsByRole(role.getId());
    +            if (accounts == null || accounts.isEmpty()) {
    +                roleVO.setRoleType(roleType);
    +            } else {
    +                throw new PermissionDeniedException("Found accounts that have role in use, won't allow to change role type");
    +            }
    +        }
    +        if (!Strings.isNullOrEmpty(description)) {
    +            roleVO.setDescription(description);
    +        }
    +        return roleDao.update(role.getId(), roleVO);
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_DELETE, eventDescription = "deleting Role")
    +    public boolean deleteRole(final Role role) {
    +        checkCallerAccess();
    +        if (role == null) {
    +            return false;
    +        }
    +        if (role.getId() <= RoleType.User.getId()) {
    +            throw new PermissionDeniedException("Default roles cannot be deleted");
    +        }
    +        List<? extends Account> accounts = accountDao.findAccountsByRole(role.getId());
    +        if (accounts == null || accounts.size() == 0) {
    +            return Transaction.execute(new TransactionCallback<Boolean>() {
    +                @Override
    +                public Boolean doInTransaction(TransactionStatus status) {
    +                    List<? extends RolePermission> rolePermissions = rolePermissionsDao.findAllByRoleId(role.getId());
    +                    if (rolePermissions != null && !rolePermissions.isEmpty()) {
    +                        for (RolePermission rolePermission : rolePermissions) {
    +                            rolePermissionsDao.remove(rolePermission.getId());
    +                        }
    +                    }
    +                    return roleDao.remove(role.getId());
    +                }
    +            });
    +        }
    +        throw new PermissionDeniedException("Found accounts that have role in use, won't allow to delete role");
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_CREATE, eventDescription = "creating Role Permission")
    +    public RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description) {
    +        checkCallerAccess();
    +        return Transaction.execute(new TransactionCallback<RolePermissionVO>() {
    +            @Override
    +            public RolePermissionVO doInTransaction(TransactionStatus status) {
    +                return rolePermissionsDao.persist(new RolePermissionVO(role.getId(), rule.toString(), permission, description));
    +            }
    +        });
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_UPDATE, eventDescription = "updating Role Permission")
    +    public boolean updateRolePermission(final RolePermission rolePermission, final Rule rule, final RolePermission.Permission permission, final String description) {
    +        checkCallerAccess();
    +        if (rolePermission == null) {
    +            return false;
    +        }
    +        RolePermissionVO rolePermissionVO = (RolePermissionVO) rolePermission;
    +        if (rule != null) {
    +            rolePermissionVO.setRule(rule.toString());
    +        }
    +        if (permission != null) {
    +            rolePermissionVO.setPermission(permission);
    +        }
    +        if (!Strings.isNullOrEmpty(description)) {
    +            rolePermissionVO.setDescription(description);
    +        }
    +        return rolePermissionsDao.update(rolePermission.getId(), rolePermissionVO);
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_DELETE, eventDescription = "deleting Role Permission")
    +    public boolean deleteRolePermission(final RolePermission rolePermission) {
    +        checkCallerAccess();
    +        return rolePermission != null && rolePermissionsDao.remove(rolePermission.getId());
    +    }
    +
    +    private List<Role> toRoleList(final List<? extends Role> roles) {
    +        if (roles != null) {
    +            return new ArrayList<>(roles);
    +        }
    +        return new ArrayList<>();
    --- End diff --
    
    Fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60881499
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    +
    +                } catch (SQLException e) {
    +                    s_logger.error("Failed to update cloud.account role_id for account id:" + accountId + " with exception: " + e.getMessage());
    +                    throw new CloudRuntimeException("Exception while updating cloud.account role_id", e);
    +                }
    +            }
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Exception while migrating existing account table's role_id column to a role based on account type", e);
    +        }
    +        s_logger.debug("Done migrating existing accounts to use one of default roles based on account type");
    +    }
    +
    +    private void setupRolesAndPermissionsForDynamicRBAC(final Connection conn) {
    +        try ( PreparedStatement selectStatement = conn.prepareStatement("SELECT * FROM `cloud`.`roles`");
    +              ResultSet resultSet = selectStatement.executeQuery()) {
    --- End diff --
    
    Fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214240215
  
    I've fixed all outstanding issues, please comment if you see any outstanding issue.
    LGTMs welcome, thanks.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59419363
  
    --- Diff: tools/marvin/marvin/cloudstackConnection.py ---
    @@ -143,7 +143,7 @@ def __sign(self, payload):
                 ["=".join(
                     [str.lower(r[0]),
                      str.lower(
    -                     urllib.quote_plus(str(r[1]))
    +                     urllib.quote_plus(str(r[1]), safe="*")
    --- End diff --
    
    This is ported from cloudmonkey -- https://github.com/apache/cloudstack-cloudmonkey/pull/11/files
    When tests send * in the cmd arg, test failures happened due to how url encoding differs across ACS specific Java code and Python


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-212071544
  
    @bhaisaab my environment builds it twice.  Once with packaging and once without.  If I have the problem again, I will send you the command that was run that had the problem.  I am running it again now, so I will let you know later tonight (well tomorrow morning for you).  :)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59514818
  
    --- Diff: engine/schema/src/com/cloud/user/AccountVO.java ---
    @@ -83,8 +86,16 @@ public AccountVO(String accountName, long domainId, String networkDomain, short
             this.domainId = domainId;
             this.networkDomain = networkDomain;
             this.type = type;
    -        state = State.enabled;
    +        this.state = State.enabled;
             this.uuid = uuid;
    +        this.roleId = RoleType.getRoleByAccountType(null, type);
    +    }
    +
    +    public AccountVO(String accountName, long domainId, String networkDomain, short type, Long roleId, String uuid) {
    +        this(accountName, domainId, networkDomain, type, uuid);
    --- End diff --
    
    all these params can be final right? (probably missed that one in all the code above)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59852709
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/ListRolesCmd.java ---
    @@ -0,0 +1,118 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.exception.ConcurrentOperationException;
    +import com.cloud.exception.InsufficientCapacityException;
    +import com.cloud.exception.NetworkRuleConflictException;
    +import com.cloud.exception.ResourceAllocationException;
    +import com.cloud.exception.ResourceUnavailableException;
    +import com.cloud.user.Account;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.ListResponse;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +@APICommand(name = ListRolesCmd.APINAME, description = "Lists dynamic roles in CloudStack", responseObject = RoleResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin})
    +public class ListRolesCmd extends BaseCmd {
    +    public static final String APINAME = "listRoles";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = RoleResponse.class, description = "List role by role ID.")
    +    private Long id;
    +
    +    @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "List role by role name.")
    +    private String roleName;
    +
    +    @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "List role by role type, valid options are: Admin, ResourceAdmin, DomainAdmin, User.")
    +    private String roleType;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public Long getId() {
    +        return id;
    +    }
    +
    +    public String getName() {
    +        return roleName;
    +    }
    +
    +    public RoleType getRoleType() {
    +        if (!Strings.isNullOrEmpty(roleType)) {
    +            return RoleType.valueOf(roleType);
    +        }
    +        return null;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
    +        if (getId() != null && getId() < 1L) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    +        List<Role> roles = roleService.findAllRolesBy(getId(), getName(), getRoleType());
    +        ListResponse<RoleResponse> response = new ListResponse<>();
    +        List<RoleResponse> roleResponses = new ArrayList<>();
    +        for (Role role : roles) {
    +            if (role == null) {
    +                continue;
    +            }
    +            RoleResponse roleResponse = new RoleResponse();
    +            roleResponse.setId(role.getUuid());
    +            roleResponse.setRoleName(role.getName());
    +            roleResponse.setRoleType(role.getRoleType());
    +            roleResponse.setDescription(role.getDescription());
    +            roleResponse.setObjectName("role");
    +            roleResponses.add(roleResponse);
    +        }
    +        response.setResponses(roleResponses);
    +        response.setResponseName(getCommandName());
    +        setResponseObject(response);
    +    }
    --- End diff --
    
    fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215389142
  
    +1 on the feature. Based on testing done on the simulator with root admin and normal user for a few APIs
    +0 on the immediate removal of command.properties/static checker for new installs and making dynamic checker as the only option for authorization.
    Reason: Any blocker (including security bugs) in this will make the 4.9 release unusable for new installs as this the only authorisation mechanism for entering into the system. It can still be used for pilot/test purposes though.
    
    @rhtyd Who is saying to stop innovating and improving? I am only talking about stability and choice.
    @jburwell Hopefully the above reason explains how this is different from other features (which impacts only a certain part of the product). Regarding writing a plugin, I meant that if someone really needed a functionality like this, they could have easily developed it outside of Cloudstack as well (like some kind of portal calling Cloudstack APIs).



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60388741
  
    --- Diff: api/src/org/apache/cloudstack/acl/Rule.java ---
    @@ -0,0 +1,65 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.google.common.base.Strings;
    +
    +import java.util.regex.Pattern;
    +
    +public final class Rule {
    +    private final String rule;
    +    private final static Pattern ALLOWED_PATTERN = Pattern.compile("^[a-zA-Z0-9*]+$");
    +
    +    public Rule(final String rule) {
    +        validate(rule);
    +        this.rule = rule;
    +    }
    +
    +    public boolean matches(final String commandName) {
    +        if (Strings.isNullOrEmpty(commandName)) {
    +            return false;
    +        }
    +        if (isWildcard()) {
    +            if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                return true;
    +            }
    +        } else {
    +            if (commandName.equalsIgnoreCase(rule)) {
    +                return true;
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isWildcard() {
    +        return rule.contains("*");
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return rule;
    +    }
    +
    +    private static boolean validate(final String rule) throws InvalidParameterValueException {
    +        if (Strings.isNullOrEmpty(rule) || !ALLOWED_PATTERN.matcher(rule).matches()) {
    +            throw new InvalidParameterValueException("Invalid rule provided. Only API names and wildcards are allowed.");
    +        }
    +        return true;
    --- End diff --
    
    @jburwell I've refactored and move all rule related methods in this class and added more unit tests. The rule itself is immutable only compo-sable by the constructor.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59856583
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java ---
    @@ -117,6 +123,12 @@ public long getEntityOwnerId() {
     
         @Override
         public void execute() {
    +        if (Strings.isNullOrEmpty(getCfgName())) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty configuration name provided");
    +        }
    +        if (getCfgName().equalsIgnoreCase(RoleService.EnableDynamicApiChecker.key())) {
    +            throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "Restricted configuration update not allowed");
    --- End diff --
    
    fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216231216
  
    I will make sure this gets into 4.9 because I think it is really needed.  We do need one more LGTM on the code.  Also, since this PR does have some implications regarding access to the system we should be pretty cautious.  I am pretty confident with a new install because that is what my CI has tested.  I think it would be prudent for us to do more testing of some upgrade scenarios to make sure the code behaves as expected.  I don't think this code should come into play with upgrades because the old properties files should be ready, but I think we need to validate the code behaves as expected.  Has anyone tested this yet?
    
    I think we should be testing:
    - Upgrading to 4.9 and NOT using this feature.
    - Upgrading to 4.9 and running the `migrate-dynamicroles.py` script to import the existing roles.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59858459
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    +
    +                } catch (SQLException e) {
    +                    s_logger.error("Failed to update cloud.account role_id for account id:" + accountId + " with exception: " + e.getMessage());
    +                    throw new CloudRuntimeException("Exception while updating cloud.account role_id", e);
    +                }
    +            }
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Exception while migrating existing account table's role_id column to a role based on account type", e);
    +        }
    +        s_logger.debug("Done migrating existing accounts to use one of default roles based on account type");
    +    }
    +
    +    private void setupRolesAndPermissionsForDynamicRBAC(final Connection conn) {
    +        try ( PreparedStatement selectStatement = conn.prepareStatement("SELECT * FROM `cloud`.`roles`");
    +              ResultSet resultSet = selectStatement.executeQuery()) {
    --- End diff --
    
    no need, multiple resources should be automatically closed with try-with-resource syntax


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59773107
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleType.java ---
    @@ -16,18 +16,90 @@
     // under the License.
     package org.apache.cloudstack.acl;
     
    +import com.cloud.user.Account;
    +import com.google.common.base.Enums;
    +import com.google.common.base.Strings;
    +
     // Enum for default roles in CloudStack
     public enum RoleType {
    -    Admin(1), ResourceAdmin(2), DomainAdmin(4), User(8), Unknown(0);
    +    Admin(1L, Account.ACCOUNT_TYPE_ADMIN, 1),
    +    ResourceAdmin(2L, Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN, 2),
    +    DomainAdmin(3L, Account.ACCOUNT_TYPE_DOMAIN_ADMIN, 4),
    +    User(4L, Account.ACCOUNT_TYPE_NORMAL, 8),
    +    Unknown(-1L, (short) -1, 0);
     
    +    private long id;
    +    private short accountType;
         private int mask;
     
    -    private RoleType(int mask) {
    +    RoleType(final long id, final short accountType, final int mask) {
    +        this.id = id;
    +        this.accountType = accountType;
             this.mask = mask;
         }
     
    -    public int getValue() {
    +    public long getId() {
    +        return id;
    +    }
    +
    +    public short getAccountType() {
    +        return accountType;
    +    }
    +
    +    public int getMask() {
             return mask;
         }
    -}
     
    +    public static RoleType fromString(final String name) {
    +        if (!Strings.isNullOrEmpty(name)
    +                && Enums.getIfPresent(RoleType.class, name).isPresent()) {
    +            return RoleType.valueOf(name);
    +        }
    +        return null;
    +    }
    +
    +    public static RoleType fromMask(int mask) {
    +        for (RoleType roleType : RoleType.values()) {
    +            if (roleType.getMask() == mask) {
    +                return roleType;
    +            }
    +        }
    +        return Unknown;
    +    }
    +
    +    public static RoleType getByAccountType(final short accountType) {
    +        RoleType roleType = RoleType.Unknown;
    +        switch (accountType) {
    +            case Account.ACCOUNT_TYPE_ADMIN:
    +                roleType = RoleType.Admin;
    +                break;
    +            case Account.ACCOUNT_TYPE_DOMAIN_ADMIN:
    +                roleType = RoleType.DomainAdmin;
    +                break;
    +            case Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN:
    +                roleType = RoleType.ResourceAdmin;
    --- End diff --
    
    All four default cloudstack roles -- Admin, DomainAdmin, ResourceAdmin and User. It's just an enum class, that maps these role types to account types. This method previously existed in account mgr impl, I moved it here.
    They've always existed. I could not find a previous FS on this, this is the oldest document on wiki referring to RoleTypes https://cwiki.apache.org/confluence/display/CLOUDSTACK/Annotations+use+in+the+API


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59853146
  
    --- Diff: engine/schema/src/com/cloud/user/AccountVO.java ---
    @@ -83,8 +86,16 @@ public AccountVO(String accountName, long domainId, String networkDomain, short
             this.domainId = domainId;
             this.networkDomain = networkDomain;
             this.type = type;
    -        state = State.enabled;
    +        this.state = State.enabled;
             this.uuid = uuid;
    +        this.roleId = RoleType.getRoleByAccountType(null, type);
    +    }
    +
    +    public AccountVO(String accountName, long domainId, String networkDomain, short type, Long roleId, String uuid) {
    +        this(accountName, domainId, networkDomain, type, uuid);
    --- End diff --
    
    fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60743961
  
    --- Diff: server/src/org/apache/cloudstack/acl/RoleManagerImpl.java ---
    @@ -0,0 +1,273 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.event.ActionEvent;
    +import com.cloud.event.EventTypes;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.dao.AccountDao;
    +import com.cloud.utils.PropertiesUtil;
    +import com.cloud.utils.component.ManagerBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.cloud.utils.db.Transaction;
    +import com.cloud.utils.db.TransactionCallback;
    +import com.cloud.utils.db.TransactionStatus;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.dao.RoleDao;
    +import org.apache.cloudstack.acl.dao.RolePermissionsDao;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.command.admin.acl.CreateRoleCmd;
    +import org.apache.cloudstack.api.command.admin.acl.CreateRolePermissionCmd;
    +import org.apache.cloudstack.api.command.admin.acl.DeleteRoleCmd;
    +import org.apache.cloudstack.api.command.admin.acl.DeleteRolePermissionCmd;
    +import org.apache.cloudstack.api.command.admin.acl.ListRolePermissionsCmd;
    +import org.apache.cloudstack.api.command.admin.acl.ListRolesCmd;
    +import org.apache.cloudstack.api.command.admin.acl.UpdateRoleCmd;
    +import org.apache.cloudstack.api.command.admin.acl.UpdateRolePermissionCmd;
    +import org.apache.cloudstack.context.CallContext;
    +import org.apache.cloudstack.framework.config.ConfigKey;
    +import org.apache.cloudstack.framework.config.Configurable;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import java.io.File;
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +@Local(value = {RoleService.class})
    +public class RoleManagerImpl extends ManagerBase implements RoleService, Configurable, PluggableService {
    +    @Inject
    +    private AccountDao accountDao;
    +    @Inject
    +    private RoleDao roleDao;
    +    @Inject
    +    private RolePermissionsDao rolePermissionsDao;
    +
    +    private void checkCallerAccess() {
    +        if (!isEnabled()) {
    +            throw new PermissionDeniedException("Dynamic api checker is not enabled, aborting role operation");
    +        }
    +        Account caller = CallContext.current().getCallingAccount();
    +        if (caller == null || caller.getRoleId() == null) {
    +            throw new PermissionDeniedException("Restricted API called by an invalid user account");
    +        }
    +        Role callerRole = findRole(caller.getRoleId());
    +        if (callerRole == null || callerRole.getRoleType() != RoleType.Admin) {
    +            throw new PermissionDeniedException("Restricted API called by an user account of non-Admin role type");
    +        }
    +    }
    +
    +    @Override
    +    public boolean isEnabled() {
    +        File apiCmdFile = PropertiesUtil.findConfigFile(PropertiesUtil.getDefaultApiCommandsFileName());
    +        return RoleService.EnableDynamicApiChecker.value() && (apiCmdFile == null || !apiCmdFile.exists());
    +    }
    +
    +    @Override
    +    public Role findRole(final Long id) {
    +        if (id == null || id < 1L) {
    +            return null;
    +        }
    +        return roleDao.findById(id);
    +    }
    +
    +    @Override
    +    public RolePermission findRolePermission(final Long id) {
    +        if (id == null) {
    +            return null;
    +        }
    +        return rolePermissionsDao.findById(id);
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_CREATE, eventDescription = "creating Role")
    +    public Role createRole(final String name, final RoleType roleType, final String description) {
    +        checkCallerAccess();
    +        if (roleType == null || roleType == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
    +        }
    +        return Transaction.execute(new TransactionCallback<RoleVO>() {
    +            @Override
    +            public RoleVO doInTransaction(TransactionStatus status) {
    +                return roleDao.persist(new RoleVO(name, roleType, description));
    +            }
    +        });
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_UPDATE, eventDescription = "updating Role")
    +    public boolean updateRole(final Role role, final String name, final RoleType roleType, final String description) {
    +        checkCallerAccess();
    +        if (role == null) {
    +            return false;
    +        }
    +        if (roleType != null && roleType == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unknown is not a valid role type");
    +        }
    +        RoleVO roleVO = (RoleVO) role;
    +        if (!Strings.isNullOrEmpty(name)) {
    +            roleVO.setName(name);
    +        }
    +        if (roleType != null) {
    +            if (role.getId() <= RoleType.User.getId()) {
    +                throw new PermissionDeniedException("The role type of default roles cannot be changed");
    +            }
    +            List<? extends Account> accounts = accountDao.findAccountsByRole(role.getId());
    +            if (accounts == null || accounts.isEmpty()) {
    +                roleVO.setRoleType(roleType);
    +            } else {
    +                throw new PermissionDeniedException("Found accounts that have role in use, won't allow to change role type");
    +            }
    +        }
    +        if (!Strings.isNullOrEmpty(description)) {
    +            roleVO.setDescription(description);
    +        }
    +        return roleDao.update(role.getId(), roleVO);
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_DELETE, eventDescription = "deleting Role")
    +    public boolean deleteRole(final Role role) {
    +        checkCallerAccess();
    +        if (role == null) {
    +            return false;
    +        }
    +        if (role.getId() <= RoleType.User.getId()) {
    +            throw new PermissionDeniedException("Default roles cannot be deleted");
    +        }
    +        List<? extends Account> accounts = accountDao.findAccountsByRole(role.getId());
    +        if (accounts == null || accounts.size() == 0) {
    +            return Transaction.execute(new TransactionCallback<Boolean>() {
    +                @Override
    +                public Boolean doInTransaction(TransactionStatus status) {
    +                    List<? extends RolePermission> rolePermissions = rolePermissionsDao.findAllByRoleId(role.getId());
    +                    if (rolePermissions != null && !rolePermissions.isEmpty()) {
    +                        for (RolePermission rolePermission : rolePermissions) {
    +                            rolePermissionsDao.remove(rolePermission.getId());
    +                        }
    +                    }
    +                    return roleDao.remove(role.getId());
    +                }
    +            });
    +        }
    +        throw new PermissionDeniedException("Found accounts that have role in use, won't allow to delete role");
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_CREATE, eventDescription = "creating Role Permission")
    +    public RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description) {
    +        checkCallerAccess();
    +        return Transaction.execute(new TransactionCallback<RolePermissionVO>() {
    +            @Override
    +            public RolePermissionVO doInTransaction(TransactionStatus status) {
    +                return rolePermissionsDao.persist(new RolePermissionVO(role.getId(), rule.toString(), permission, description));
    +            }
    +        });
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_UPDATE, eventDescription = "updating Role Permission")
    +    public boolean updateRolePermission(final RolePermission rolePermission, final Rule rule, final RolePermission.Permission permission, final String description) {
    +        checkCallerAccess();
    +        if (rolePermission == null) {
    +            return false;
    +        }
    +        RolePermissionVO rolePermissionVO = (RolePermissionVO) rolePermission;
    +        if (rule != null) {
    +            rolePermissionVO.setRule(rule.toString());
    +        }
    +        if (permission != null) {
    +            rolePermissionVO.setPermission(permission);
    +        }
    +        if (!Strings.isNullOrEmpty(description)) {
    +            rolePermissionVO.setDescription(description);
    +        }
    +        return rolePermissionsDao.update(rolePermission.getId(), rolePermissionVO);
    +    }
    +
    +    @Override
    +    @ActionEvent(eventType = EventTypes.EVENT_ROLE_PERMISSION_DELETE, eventDescription = "deleting Role Permission")
    +    public boolean deleteRolePermission(final RolePermission rolePermission) {
    +        checkCallerAccess();
    +        return rolePermission != null && rolePermissionsDao.remove(rolePermission.getId());
    +    }
    +
    +    private List<Role> toRoleList(final List<? extends Role> roles) {
    --- End diff --
    
    Have you considered this method to a common utility method?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60880846
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    --- End diff --
    
    I'm not sure, we can refactor this at a later stage if needed.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59853322
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/CreateRolePermissionCmd.java ---
    @@ -0,0 +1,121 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.user.Account;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RolePermission;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.acl.Rule;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.RolePermissionResponse;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +import org.apache.cloudstack.context.CallContext;
    +
    +@APICommand(name = CreateRolePermissionCmd.APINAME, description = "Adds a API permission to a role", responseObject = RolePermissionResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class CreateRolePermissionCmd extends BaseCmd {
    +    public static final String APINAME = "createRolePermission";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.ROLE_ID, type = CommandType.UUID, required = true, entityType = RoleResponse.class, description = "ID of the role")
    +    private Long roleId;
    +
    +    @Parameter(name = ApiConstants.RULE, type = CommandType.STRING, required = true, description = "The API name or wildcard rule such as list*")
    +    private String rule;
    +
    +    @Parameter(name = ApiConstants.PERMISSION, type = CommandType.STRING, required = true, description = "The rule permission, allow or deny. Default: deny.")
    +    private String permission;
    +
    +    @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the role permission")
    +    private String description;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public Long getRoleId() {
    +        return roleId;
    +    }
    +
    +    public Rule getRule() {
    +        return new Rule(rule);
    +    }
    +
    +    public RolePermission.Permission getPermission() {
    +        if (Strings.isNullOrEmpty(permission)) {
    +            return null;
    +        }
    +        return RolePermission.Permission.valueOf(permission.toUpperCase());
    +    }
    +
    +    public String getDescription() {
    +        return description;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() {
    +        if (getRule() == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role permission rule provided");
    +        }
    +        Role role = roleService.findRole(getRoleId());
    +        if (role == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    +        CallContext.current().setEventDetails("Role id: " + role.getId() + ", rule:" + getRule() + ", permission: " + getPermission() + ", description: " + getDescription());
    +        RolePermission rolePermission = roleService.createRolePermission(role, getRule(), getPermission(), getDescription());
    +        if (rolePermission == null) {
    +            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create role permission");
    +        }
    +
    +        RolePermissionResponse response = new RolePermissionResponse();
    +        response.setId(rolePermission.getUuid());
    +        response.setRoleId(role.getUuid());
    +        response.setRule(rolePermission.getRule());
    +        response.setRulePermission(rolePermission.getPermission());
    +        response.setDescription(rolePermission.getDescription());
    +        response.setResponseName(getCommandName());
    +        response.setObjectName("rolepermission");
    +        setResponseObject(response);
    +     }
    --- End diff --
    
    fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60771393
  
    --- Diff: plugins/user-authenticators/ldap/src/org/apache/cloudstack/api/command/LdapImportUsersCmd.java ---
    @@ -131,7 +135,9 @@ private void createCloudstackUserAccount(LdapUser user, String accountName, Doma
         @Override
         public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException,
             ResourceAllocationException, NetworkRuleConflictException {
    -
    +        if (getAccountType() == null && getRoleId() == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Both account type and role ID are not provided");
    --- End diff --
    
    same reasons from above 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216228334
  
    The PR is ready for merge, any testing/feedback welcome /cc @swill 
    
    tag:mergeready


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215291402
  
    The CI results show this PR to be in pretty good shape.  We have one LGTM and lots of commentary.  Can we get a summary of any concerns and get our code reviews in?  Thanks...


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60356809
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    --- End diff --
    
    Instead of doing check in Java code, better to filter at DB itself to get only allow or deny rules.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60879938
  
    --- Diff: utils/src/main/java/com/cloud/utils/PropertiesUtil.java ---
    @@ -34,6 +34,10 @@
     public class PropertiesUtil {
         private static final Logger s_logger = Logger.getLogger(PropertiesUtil.class);
     
    +    public static String getDefaultApiCommandsFileName() {
    +        return "commands.properties";
    +    }
    --- End diff --
    
    commands.properties is consumed by server, plugins/acl/static-role-based and engine/schema packages. I'm keeping it this way so it can be consumed by all these packages, as utils is used by all and PropertiesUtils is a utility class to manage .properties files


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-212106389
  
    The tests are now being run, so this time everything built alright.  I will have results for you later tonight or in the morning.  :)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59854979
  
    --- Diff: api/src/org/apache/cloudstack/acl/Rule.java ---
    @@ -0,0 +1,42 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.google.common.base.Strings;
    +
    +public final class Rule {
    +    private final String rule;
    +    private final static String ALLOWED_PATTERN = "^[a-zA-Z0-9*]+$";
    +
    +    public Rule(final String rule) {
    +        validate(rule);
    +        this.rule = rule;
    +    }
    +
    +    public String toString() {
    +        return rule;
    +    }
    +
    +    public static boolean validate(final String rule) throws InvalidParameterValueException {
    +        if (Strings.isNullOrEmpty(rule) || !rule.matches(ALLOWED_PATTERN)) {
    --- End diff --
    
    Fixed Pattern usage. We've toString() and the constructor making sure its immutable and validated. We don't need explicit equals() which will compare the rule string, I've fix the validate() to be a private method so the only way to build/interact with Rule is by creating an (immutable) readonly object. Anything else I should do?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59800354
  
    --- Diff: api/test/org/apache/cloudstack/acl/RoleTypeTest.java ---
    @@ -0,0 +1,92 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.user.Account;
    +import org.junit.Assert;
    +import org.junit.Test;
    +import org.mockito.Mockito;
    +
    +import java.util.Arrays;
    +
    +public class RoleTypeTest {
    +
    +    @Test
    +    public void testRoleTypeFromString() {
    +        Assert.assertEquals(RoleType.fromString(null), null);
    +        Assert.assertEquals(RoleType.fromString(""), null);
    +        Assert.assertEquals(RoleType.fromString("admin"), null);
    +        Assert.assertEquals(RoleType.fromString("12345%^&*"), null);
    --- End diff --
    
    Why doesn't ``fromString`` should either return ``Unknown`` or throw an ``IllegalStateException`` in these scenarios?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60390739
  
    --- Diff: engine/schema/src/org/apache/cloudstack/acl/RolePermissionVO.java ---
    @@ -0,0 +1,109 @@
    +// 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.cloudstack.acl;
    +
    +import javax.persistence.Column;
    +import javax.persistence.Entity;
    +import javax.persistence.EnumType;
    +import javax.persistence.Enumerated;
    +import javax.persistence.GeneratedValue;
    +import javax.persistence.GenerationType;
    +import javax.persistence.Id;
    +import javax.persistence.Table;
    +import java.util.UUID;
    +
    +@Entity
    +@Table(name = "role_permissions")
    +public class RolePermissionVO implements RolePermission {
    --- End diff --
    
    if you re-read my reply and see the static-checker and dynamic checker code -- backward compatibility is to ensure that we deny for all when no rule matches; for backward compatibility the dynamic checker also needs to check the annotation map. The dynamic checker allows for wildcard rules, and if you want to override annotations we'll need a set of deny rules before that. Therefore we need deny rules explicitly. This way of implementation makes dynamic-checker a drop-in replacement at the same time allows for wider use-cases and acl management.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59858432
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    +
    +                } catch (SQLException e) {
    +                    s_logger.error("Failed to update cloud.account role_id for account id:" + accountId + " with exception: " + e.getMessage());
    +                    throw new CloudRuntimeException("Exception while updating cloud.account role_id", e);
    +                }
    +            }
    --- End diff --
    
    at the time of upgrade there will be only a single thread running this, performing the upgrades so no explicit need of a transaction. The exception is thrown so that it can appear to the sysadmin as something needing manual fixing.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59853380
  
    --- Diff: server/src/org/apache/cloudstack/acl/RoleManagerImpl.java ---
    @@ -0,0 +1,242 @@
    +package org.apache.cloudstack.acl;
    --- End diff --
    
    Fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59801354
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    --- End diff --
    
    The ``selectResultSet`` is a resource that needs to be closed.  Please add it to enclosing try with resources block.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-211262271
  
    @swill I've fixed the outstanding issues, can you run your CI on this and help merge? thanks


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60369373
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            final String rule = permission.getRule();
    +            if (rule.contains("*")) {
    +                if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                    return true;
    +                }
    +            } else {
    +                if (commandName.equals(rule)) {
    +                    return true;
    +                }
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    --- End diff --
    
    @koushik-das the default if everything else falls through is deny due to backward compatibility with static-checker (as this feature needs to be a drop-in replacement and still work out of the box)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215165382
  
    @mlsorensen 
    - presently, setting bitmask to 0 in commands.properties does not disable the API but that no roles would have that API; though if an API through its `authroized` annotation enables itself for a roletype that will be considered
    - this feature implements a DENY rule that is checked before annotations are checked to provide a mechanism to disable an API (or group of APIs if a wildcard rule is used) that could have been enabled by the annotation (for example)
    
    @koushik-das 
    - Existing users are not migrated automatically, they continue to use static-checker
    - Commands.properties file is only removed from codebase, after an upgrade this file won't be removed by the deb/rpm packages
    - New users and installations are no longer encouraged to use the old static-checker, therefore with the aim of deprecating the static-checker over time dynamic-checker is enabled by default
    - Any change can introduce side-effects and bugs but that does not mean we should stop innovating or stop improving cloudstack; such an attitude would be harmful for any project
    - I really appreciate your code review, improvement suggestions and testing feedback instead, on this PR. Thanks.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220094101
  
    @rhtyd thank you sir.  I am trying to get everything ready to freeze.  Damn this is a lot of work.  :(


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-218370503
  
    Thanks you @swill finally :smile: 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220093571
  
    @swill yes sir. I'll personally make sure to perform another test round tomorrow both with UI, marvin and APIs/cloudmonkey.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216559351
  
    Great, thanks for the additional details @borisstoyanov.  \U0001f44d   Once Rohit and John are in agreement on the final details, I think we are ready to merge this one.  Thanks everyone...
    
    Sorry for accidentally introducing you into this @borisroman.  :)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59856707
  
    --- Diff: api/test/org/apache/cloudstack/acl/RoleTypeTest.java ---
    @@ -0,0 +1,92 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.user.Account;
    +import org.junit.Assert;
    +import org.junit.Test;
    +import org.mockito.Mockito;
    +
    +import java.util.Arrays;
    +
    +public class RoleTypeTest {
    +
    +    @Test
    +    public void testRoleTypeFromString() {
    +        Assert.assertEquals(RoleType.fromString(null), null);
    +        Assert.assertEquals(RoleType.fromString(""), null);
    +        Assert.assertEquals(RoleType.fromString("admin"), null);
    +        Assert.assertEquals(RoleType.fromString("12345%^&*"), null);
    +        for (RoleType roleType : RoleType.values()) {
    +            Assert.assertEquals(RoleType.fromString(roleType.name()), roleType);
    +        }
    +    }
    +
    +    @Test
    +    public void testDefaultRoleMaskByValue() {
    +        Assert.assertEquals(RoleType.fromMask(1), RoleType.Admin);
    +        Assert.assertEquals(RoleType.fromMask(2), RoleType.ResourceAdmin);
    +        Assert.assertEquals(RoleType.fromMask(4), RoleType.DomainAdmin);
    +        Assert.assertEquals(RoleType.fromMask(8), RoleType.User);
    +        Assert.assertEquals(RoleType.fromMask(0), RoleType.Unknown);
    --- End diff --
    
    We get `Unknown`, for example 0 is an invalid mask value here.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-209026193
  
    Some screenshots \*
    
    Roles Tab:
    ![Uploading Screenshot from 2016-04-12 23-11-42.png…]()
    
    Add a new role:
    ![screenshot from 2016-04-12 23-11-48](https://cloud.githubusercontent.com/assets/95203/14469969/a6ccc7dc-0104-11e6-9079-ced744f677a6.png)
    
    Adding rules to a role with API auto-completion ** (API and wildcards allowed):
    ![screenshot from 2016-04-12 23-12-32](https://cloud.githubusercontent.com/assets/95203/14469974/ace835c0-0104-11e6-812d-3a8970ad4616.png)
    
    \* feature available for root admin only
    \*\* no mgmt server restarts needed



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60738403
  
    --- Diff: scripts/util/migrate-dynamicroles.py ---
    @@ -0,0 +1,134 @@
    +#!/usr/bin/python
    +# -*- coding: utf-8 -*-
    +# 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.
    +
    +import os
    +import sys
    +import uuid
    +
    +from contextlib import closing
    +from optparse import OptionParser
    +
    +try:
    +    import MySQLdb
    +except ImportError:
    +    print("MySQLdb cannot be imported, please install python-mysqldb(apt) or mysql-python(yum)")
    +    sys.exit(1)
    +
    +dryrun = False
    +
    +
    +def runSql(conn, query):
    +    if dryrun:
    +        print("Running SQL query: " + query)
    +        return
    +    with closing(conn.cursor()) as cursor:
    +        cursor.execute(query)
    +
    +
    +def migrateApiRolePermissions(apis, conn):
    +    # All allow for root admin role Admin(id:1)
    +    runSql(conn, "INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), 1, '*', 'Allow');")
    +    # Migrate rules based on commands.properties rule for ResourceAdmin(id:2), DomainAdmin(id:3), User(id:4)
    +    octetKey = {2:2, 3:4, 4:8}
    +    for role in [2, 3, 4]:
    +        for api in sorted(apis.keys()):
    +            # Ignore auth commands
    +            if api in ['login', 'logout', 'samlSso', 'samlSlo', 'listIdps', 'listAndSwitchSamlAccount', 'getSPMetadata']:
    +                continue
    +            if (octetKey[role] & int(apis[api])) > 0:
    +                runSql(conn, "INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'Allow');" % (role, api))
    +
    +
    +def main():
    +    parser = OptionParser()
    +    parser.add_option("-b", "--db", action="store", type="string", dest="db", default="cloud",
    +                        help="The name of the database, default: cloud")
    +    parser.add_option("-u", "--user", action="store", type="string", dest="user", default="cloud",
    +                        help="User name a MySQL user with privileges on cloud database")
    +    parser.add_option("-p", "--password", action="store", type="string", dest="password", default="cloud",
    +                        help="Password of a MySQL user with privileges on cloud database")
    +    parser.add_option("-H", "--host", action="store", type="string", dest="host", default="127.0.0.1",
    +                        help="Host or IP of the MySQL server")
    +    parser.add_option("-P", "--port", action="store", type="int", dest="port", default=3306,
    +                        help="Host or IP of the MySQL server")
    +    parser.add_option("-f", "--properties-file", action="store", type="string", dest="commandsfile", default="/etc/cloudstack/management/commands.properties",
    +                        help="The commands.properties file")
    +    parser.add_option("-d", "--dryrun", action="store_true", dest="dryrun", default=False,
    +                        help="Dry run and debug operations this tool will perform")
    +    (options, args) = parser.parse_args()
    +
    +    print("Apache CloudStack Role Permission Migration Tool")
    +    print("(c) Apache CloudStack Authors and the ASF, under the Apache License, Version 2.0\n")
    +
    +    global dryrun
    +    if options.dryrun:
    +        dryrun = True
    +
    +    conn = MySQLdb.connect(
    +            host=options.host,
    +            user=options.user,
    +            passwd=options.password,
    +            port=int(options.port),
    +            db=options.db)
    +
    +    if not os.path.isfile(options.commandsfile):
    +        print("Provided commands.properties cannot be accessed or does not exist, please check check permissions")
    +        sys.exit(1)
    +
    +    while True:
    +        choice = raw_input("Running this migration tool will remove any " +
    +                           "default-role rules in cloud.role_permissions. " +
    +                           "Do you want to continue? [y/N]").lower()
    --- End diff --
    
    Would it be possible to query the table to determine if it is empty?  Based on that information, we could give a clearer warning (i.e. "Hey, buddy, you have roles defined and this tool will blow them away" ...)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-211911369
  
    This failed to compile.  Can you have a look?
    
    ```
    [INFO] --- exec-maven-plugin:1.2.1:exec (compile) @ cloud-apidoc ---
    log4j:WARN No appenders could be found for logger (org.reflections.Reflections).
    log4j:WARN Please initialize the log4j system properly.
    log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
    Scanned and found 508 APIs
    Traceback (most recent call last):
      File "/data/git/cs2/cloudstack/tools/apidoc/gen_toc.py", line 208, in <module>
        'dirname': dirname_to_dirname[dirname],
    KeyError: 'root_admin'
    ```
    
    ```
    [INFO] Apache CloudStack Developer Mode .................. SUCCESS [1.356s]
    [INFO] Apache CloudStack Developer Tools ................. SUCCESS [0.252s]
    [INFO] Apache CloudStack apidocs ......................... FAILURE [8.407s]
    [INFO] Apache CloudStack marvin .......................... SKIPPED
    [INFO] Apache CloudStack DevCloud ........................ SKIPPED
    ```


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59516247
  
    --- Diff: server/src/org/apache/cloudstack/acl/RoleManagerImpl.java ---
    @@ -0,0 +1,242 @@
    +package org.apache.cloudstack.acl;
    --- End diff --
    
    license missing


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59786533
  
    --- Diff: api/src/org/apache/cloudstack/acl/Rule.java ---
    @@ -0,0 +1,42 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.google.common.base.Strings;
    +
    +public final class Rule {
    +    private final String rule;
    +    private final static String ALLOWED_PATTERN = "^[a-zA-Z0-9*]+$";
    +
    +    public Rule(final String rule) {
    +        validate(rule);
    +        this.rule = rule;
    +    }
    +
    +    public String toString() {
    +        return rule;
    +    }
    +
    +    public static boolean validate(final String rule) throws InvalidParameterValueException {
    +        if (Strings.isNullOrEmpty(rule) || !rule.matches(ALLOWED_PATTERN)) {
    --- End diff --
    
    Extract this validation logic into an immutable value class, ``Rule``, to encapsulate this validation logic and provide strong type specification in lower layers.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by pdube <gi...@git.apache.org>.
Github user pdube commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59730334
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleType.java ---
    @@ -16,18 +16,90 @@
     // under the License.
     package org.apache.cloudstack.acl;
     
    +import com.cloud.user.Account;
    +import com.google.common.base.Enums;
    +import com.google.common.base.Strings;
    +
     // Enum for default roles in CloudStack
     public enum RoleType {
    -    Admin(1), ResourceAdmin(2), DomainAdmin(4), User(8), Unknown(0);
    +    Admin(1L, Account.ACCOUNT_TYPE_ADMIN, 1),
    +    ResourceAdmin(2L, Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN, 2),
    +    DomainAdmin(3L, Account.ACCOUNT_TYPE_DOMAIN_ADMIN, 4),
    +    User(4L, Account.ACCOUNT_TYPE_NORMAL, 8),
    +    Unknown(-1L, (short) -1, 0);
     
    +    private long id;
    +    private short accountType;
         private int mask;
     
    -    private RoleType(int mask) {
    +    RoleType(final long id, final short accountType, final int mask) {
    +        this.id = id;
    +        this.accountType = accountType;
             this.mask = mask;
         }
     
    -    public int getValue() {
    +    public long getId() {
    +        return id;
    +    }
    +
    +    public short getAccountType() {
    +        return accountType;
    +    }
    +
    +    public int getMask() {
             return mask;
         }
    -}
     
    +    public static RoleType fromString(final String name) {
    +        if (!Strings.isNullOrEmpty(name)
    +                && Enums.getIfPresent(RoleType.class, name).isPresent()) {
    +            return RoleType.valueOf(name);
    +        }
    +        return null;
    +    }
    +
    +    public static RoleType fromMask(int mask) {
    +        for (RoleType roleType : RoleType.values()) {
    +            if (roleType.getMask() == mask) {
    +                return roleType;
    +            }
    +        }
    +        return Unknown;
    +    }
    +
    +    public static RoleType getByAccountType(final short accountType) {
    +        RoleType roleType = RoleType.Unknown;
    +        switch (accountType) {
    +            case Account.ACCOUNT_TYPE_ADMIN:
    +                roleType = RoleType.Admin;
    +                break;
    +            case Account.ACCOUNT_TYPE_DOMAIN_ADMIN:
    +                roleType = RoleType.DomainAdmin;
    +                break;
    +            case Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN:
    +                roleType = RoleType.ResourceAdmin;
    +                break;
    +            case Account.ACCOUNT_TYPE_NORMAL:
    +                roleType = RoleType.User;
    +                break;
    +        }
    +        return roleType;
    +    }
    +
    +    public static Long getRoleByAccountType(final Long roleId, final Short accountType) {
    --- End diff --
    
    Why are you passing in a roleId, if you are getting a role id by account type?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60738990
  
    --- Diff: server/src/com/cloud/api/dispatch/ParamProcessWorker.java ---
    @@ -92,6 +94,55 @@ public void handle(final DispatchTask task) {
             processParameters(task.getCmd(), task.getParams());
         }
     
    +    private void validateNonEmptyString(final Object param, final String argName) {
    +        if (param == null || Strings.isNullOrEmpty(param.toString())) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Empty or null value provided for API arg: %s", argName));
    +        }
    +    }
    +
    +    private void validateNaturalNumber(final Object param, final String argName) {
    +        Long value = null;
    +        if (param != null && param instanceof Long) {
    +            value = (Long) param;
    +        } else if (param != null) {
    +            value = Long.valueOf(param.toString());
    +        }
    +        if (value == null || value < 1L) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Invalid value provided for API arg: %s", argName));
    +        }
    +    }
    +
    +    private void validateField(final Object paramObj, final Parameter annotation) throws ServerApiException {
    +        if (annotation == null) {
    +            return;
    +        }
    +        final String argName = annotation.name();
    +        for (final ApiArgValidator validator : annotation.validations()) {
    +            if (validator == null) {
    +                continue;
    +            }
    +            switch (validator) {
    +                case NotNullOrEmpty:
    +                    switch (annotation.type()) {
    +                        case UUID:
    +                        case STRING:
    +                            validateNonEmptyString(paramObj, argName);
    +                            break;
    +                    }
    +                    break;
    +                case PositiveNumber:
    +                    switch (annotation.type()) {
    +                        case SHORT:
    +                        case INTEGER:
    +                        case LONG:
    +                            validateNaturalNumber(paramObj, argName);
    +                            break;
    +                    }
    --- End diff --
    
    Why not place these validation methods on the ``ApiArgValidator`` instances?  Not only would it simplify this code (i.e. reducing the switch to a single line), but it would also make it easier to add new validators without changing this class.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60732671
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    +
    +                } catch (SQLException e) {
    +                    s_logger.error("Failed to update cloud.account role_id for account id:" + accountId + " with exception: " + e.getMessage());
    +                    throw new CloudRuntimeException("Exception while updating cloud.account role_id", e);
    +                }
    +            }
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Exception while migrating existing account table's role_id column to a role based on account type", e);
    +        }
    +        s_logger.debug("Done migrating existing accounts to use one of default roles based on account type");
    +    }
    +
    +    private void setupRolesAndPermissionsForDynamicRBAC(final Connection conn) {
    +        try ( PreparedStatement selectStatement = conn.prepareStatement("SELECT * FROM `cloud`.`roles`");
    +              ResultSet resultSet = selectStatement.executeQuery()) {
    --- End diff --
    
    @bhaisaab if ``resultSet`` is not declared as part of the enclosing try with resources block, it won't be automatically closed.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60434048
  
    --- Diff: api/src/org/apache/cloudstack/acl/RolePermission.java ---
    @@ -0,0 +1,30 @@
    +// 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.cloudstack.acl;
    +
    +import org.apache.cloudstack.api.Identity;
    +import org.apache.cloudstack.api.InternalIdentity;
    +
    +public interface RolePermission extends InternalIdentity, Identity {
    +    enum Permission {ALLOW, DENY}
    +
    +    long getRoleId();
    +    String getRule();
    --- End diff --
    
    @bhaisaab is it possible to use ``Rule`` as the type for this method?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214723830
  
    > @rhtyd The current way you have implemented it using global config is confusing. Especially when you say that simply disabling the flag won't fall back to the static checker automatically and additional stuff needs to be done to make it work.
    
    If you read the code, the global setting is restricted, read-only that admin are not allowed to change from the UI or using the API. The intention is to not expose the logistics, and let users know that this is a one way process, discourage them from moving back to static-checker while hide the details from them. The documentation cover migration procedure among other details.
    
    From your comments, I think you are beginning to understand what you would like to have already exists. For a better understanding, it would help if you would actually read the code in the PR and the detailed FS.



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220230192
  
    @anshul1886 I'm yet to see chrome dev-tool screenshots, actual errors reported in the UI and see answers for questions I asked; again -- the role_id in accounts table, your environment info (win/linux/osx etc).
    
    Now, I cannot guarantee that this will work in your feature branch as I don't know what changes your branch has; I've only tested master with a fresh environment on a linux machine and it is working for me both using maven and using rpms http://packages.shapeblue.com/cloudstack/custom/master-rbac:
    
    ![screenshot from 2016-05-19 10-43-47](https://cloud.githubusercontent.com/assets/95203/15383250/4f5a5d8a-1daf-11e6-8857-762433bb6e45.png)
    
    Created a user with user role:
    
    ![screenshot from 2016-05-19 10-45-19](https://cloud.githubusercontent.com/assets/95203/15383260/5feb11e4-1daf-11e6-91fd-0bccf16fed91.png)
    
    Was able to log in:
    
    ![screenshot from 2016-05-19 10-45-39](https://cloud.githubusercontent.com/assets/95203/15383270/6f99c400-1daf-11e6-8623-6335b01f6cef.png)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60439905
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,170 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import org.apache.log4j.Logger;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    protected static final Logger LOGGER = Logger.getLogger(DynamicRoleBasedAPIAccessChecker.class);
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null || permissions.isEmpty() || Strings.isNullOrEmpty(commandName)) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            try {
    +                final Rule rule = new Rule(permission.getRule());
    +                if (rule.matches(commandName)) {
    +                    return true;
    +                }
    +            } catch (InvalidParameterValueException e) {
    +                LOGGER.warn("Invalid rule permission, please fix id=" + permission.getId() + " rule=" + permission.getRule());
    +                continue;
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    +            return true;
    +        }
    +
    +        // Check for deny rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.DENY, commandName)) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Check annotations
    +        if (annotationRoleBasedApisMap.get(accountRole.getRoleType()) != null
    +                && annotationRoleBasedApisMap.get(accountRole.getRoleType()).contains(commandName)) {
    +            return true;
    +        }
    +
    +        denyApiAccess(commandName);
    +        return false;
    +    }
    +
    +    public void addApiToRoleBasedAnnotationsMap(final RoleType roleType, final String commandName) {
    +        if (roleType == null || Strings.isNullOrEmpty(commandName)) {
    +            return;
    +        }
    +        final Set<String> commands = annotationRoleBasedApisMap.get(roleType);
    +        if (commands != null && !commands.contains(commandName)) {
    +            commands.add(commandName);
    +        }
    +    }
    +
    +    @Override
    +    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
    +        super.configure(name, params);
    +        return true;
    +    }
    +
    +    @Override
    +    public boolean start() {
    +        for (PluggableService service : services) {
    +            for (Class<?> clz : service.getCommands()) {
    +                APICommand command = clz.getAnnotation(APICommand.class);
    +                for (RoleType role : command.authorized()) {
    +                    addApiToRoleBasedAnnotationsMap(role, command.name());
    --- End diff --
    
    @koushik-das (2) when the class is configured and start() called, it goes through each service, loops through the exposed apis and gets the authorized list of roletypes from its APICommand annotation. It is here that it know what default role types are allowed for a given api. For most apis, authorized is empty (as developers don't use it much).


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60386634
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            final String rule = permission.getRule();
    +            if (rule.contains("*")) {
    +                if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                    return true;
    +                }
    +            } else {
    +                if (commandName.equals(rule)) {
    +                    return true;
    +                }
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    --- End diff --
    
    @koushik-das if you want to override the default set in API annotations we'll need deny permissions there it is needed. For example, let's say api deleteXyz has an authorized field in @APIParam set to {RoleType.Admin} i.e all admin type account allowed to run this API and if we want some role (of admin role type) to be denied this API we'll need a deny rule check before the annotations are checked.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216749200
  
    @jburwell The API is transactional and you're right if more than one admin decide to change the order, client side final order checking will be needed. This is also true for VPC ACL rules too, where the same sort of order is used (it also supports draggable allow/deny items in the UI).
    
    If I implement the API to apply change the order at once (instead of multiple single operations) and there are multiple root admins changing the rules, the order saved by the last user will be in effect; and client side tracking/verification will still be required.
    
     Realistically, only root admins will be using this feature and it's unlikely to hit such a case often.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-217855833
  
    We have the required code reviews and CI results.  I will add this to merge queue.  Thx...


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214212648
  
    @koushik-das this is part of the feature to be able to check access based on rules in DB and be consistent across all mgmt servers. In my local environment with stock (un-optimized) mysql server, I can do a max of 12.8k req/s  benchmarked against wrk
    
    ```
    $ wrk -t16 -c1000 -d30s  "http://localhost:8080/client/api?command=listUsers"                                                                                                       [14:08:08]
    Running 30s test @ http://localhost:8080/client/api?command=listUsers
      16 threads and 1000 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    78.35ms   64.44ms   1.52s    93.98%
        Req/Sec   810.93    171.75     1.98k    77.53%
      387964 requests in 30.09s, 147.26MB read
      Socket errors: connect 0, read 0, write 0, timeout 2
      Non-2xx or 3xx responses: 387964
    Requests/sec:  12893.98
    Transfer/sec:      4.89MB
    ```
    
    And with another query, where dynamic checker is forced to fail doing all sorts of db queries, it resulted about 700 req/s.
    ```
    $ wrk -t16 -c1000 -d30s "http://localhost:8096/client/api?signatureversion=3&apiKey=&expires=2016-04-25T08%3A50%3A19%2B0000&command=listUsers&signature=fmgUHUhRdCYf%2BoPHgcTVqzx0am4%3D&response=json&listall=true"
    Running 30s test @ http://localhost:8096/client/api?signatureversion=3&apiKey=&expires=2016-04-25T08%3A50%3A19%2B0000&command=listUsers&signature=fmgUHUhRdCYf%2BoPHgcTVqzx0am4%3D&response=json&listall=true
      16 threads and 1000 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     1.32s   197.24ms   1.79s    90.25%
        Req/Sec    72.78     91.71   570.00     89.25%
      21252 requests in 30.09s, 31.43MB read
      Socket errors: connect 0, read 0, write 0, timeout 18
    Requests/sec:    706.17
    Transfer/sec:      1.04MB
    ```
    
    @koushik-das we've db schema for consistency, we read data from commands.properties and write them to a db table. We've a test_staticroles.py too, that can do pre-upgrade integration testing and post-upgrade we've test_dynamicroles.py. Lastly, it is intended to make reverse-migration difficult to avoid inconsistent and unknown security behavior, read FS for details. If you simply turn off the restricted global setting (from true to false), it will disable both dynamic and static checker. One constraint for this to enable is that a flag in db is enabled and commands.properties file does not exist or readable from its classpath. Also, since commands.properties is removed even if you switch the flags you'll need to create this file, put in client/tomcatconf (as developer) and restart mgmt server as unlike dynamic-checker, the static checker initializes only at boot time and not runtime.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59857696
  
    --- Diff: api/test/org/apache/cloudstack/acl/RoleTypeTest.java ---
    @@ -0,0 +1,92 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.user.Account;
    +import org.junit.Assert;
    +import org.junit.Test;
    +import org.mockito.Mockito;
    +
    +import java.util.Arrays;
    +
    +public class RoleTypeTest {
    +
    +    @Test
    +    public void testRoleTypeFromString() {
    +        Assert.assertEquals(RoleType.fromString(null), null);
    +        Assert.assertEquals(RoleType.fromString(""), null);
    +        Assert.assertEquals(RoleType.fromString("admin"), null);
    +        Assert.assertEquals(RoleType.fromString("12345%^&*"), null);
    +        for (RoleType roleType : RoleType.values()) {
    +            Assert.assertEquals(RoleType.fromString(roleType.name()), roleType);
    +        }
    +    }
    +
    +    @Test
    +    public void testDefaultRoleMaskByValue() {
    +        Assert.assertEquals(RoleType.fromMask(1), RoleType.Admin);
    +        Assert.assertEquals(RoleType.fromMask(2), RoleType.ResourceAdmin);
    +        Assert.assertEquals(RoleType.fromMask(4), RoleType.DomainAdmin);
    +        Assert.assertEquals(RoleType.fromMask(8), RoleType.User);
    +        Assert.assertEquals(RoleType.fromMask(0), RoleType.Unknown);
    +    }
    +
    +    @Test
    +    public void testGetByAccountType() {
    +        Assert.assertEquals(RoleType.getByAccountType(Account.ACCOUNT_TYPE_NORMAL), RoleType.User);
    +        Assert.assertEquals(RoleType.getByAccountType(Account.ACCOUNT_TYPE_ADMIN), RoleType.Admin);
    +        Assert.assertEquals(RoleType.getByAccountType(Account.ACCOUNT_TYPE_DOMAIN_ADMIN), RoleType.DomainAdmin);
    +        Assert.assertEquals(RoleType.getByAccountType(Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN), RoleType.ResourceAdmin);
    +        Assert.assertEquals(RoleType.getByAccountType(Account.ACCOUNT_TYPE_PROJECT), RoleType.Unknown);
    +    }
    +
    +    @Test
    +    public void testGetRoleByAccountTypeWhenRoleIdIsProvided() {
    +        Assert.assertEquals(RoleType.getRoleByAccountType(123L, Account.ACCOUNT_TYPE_ADMIN), Long.valueOf(123L));
    +        Assert.assertEquals(RoleType.getRoleByAccountType(1234L, null), Long.valueOf(1234L));
    +    }
    +
    +    @Test
    +    public void testGetRoleByAccountTypeForDefaultAccountTypes() {
    +        Assert.assertEquals(RoleType.getRoleByAccountType(null, Account.ACCOUNT_TYPE_ADMIN), (Long) RoleType.Admin.getId());
    --- End diff --
    
    There is another test for testing when non-null values are passed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-218358102
  
    thank you sir.  can you repush right away again.  jenkins already failed and I like to make sure everything is green before i merge stuff.  thx...


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215352448
  
    @DaanHoogland @wido @mlsorensen @jburwell @agneya2001 @koushik-das @terbolous @resmo @K0zka  review and LGTM please?
     
    @swill I think we've resolved all outstanding concerns, and if there are still any concern that should be shared, summary;
    - Existing users are not migrated automatically, they continue to use static-checker, @koushik-das raised concern if dynamic-checker is forced upon users -- it is not
    - Commands.properties file is only removed from codebase, after an upgrade this file won't be removed by the deb/rpm packages, @koushik-das raised concern on not removing it -- it is clarified that it's not for users with existing deployments
    - Valid points raised by @mlsorensen on why dynamic checker is the way forward and static-checker has several pain points for real world users
    - @koushik-das raised concerns about code quality and testing, my colleague @borisstoyanov  and I have spent good time to test the feature against ldap, saml and native cloudstack authentication; along with upgrade and regression tests around the feature. The integration test runs with Travis as well, providing nearly 100% coverage


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214296336
  
    @rhtyd My comment regarding the test was more in the context of perf. test. In the DB for regular user I saw ~250 permissions got created. So this means iterating over all these entries twice (ALLOW and DENY) to find a match and then perform access check. There will be a perf. overhead due to this and user should have an option to decide whether to use static or dynamic. Also if the user finds some issues/bugs later during their testing there should be a fallback option.
    
    Regarding upgrade implications, I went through the docs/FS but some things are still confusing. If existing user can continue using commands.properties then what happens to the new APIs that gets added. If the argument is that the permission can be put in as an annotation in code for new APIs then that removes the flexibility of the earlier mechanism (there is no way to modify the default in code). We don't know how people are customising commands.properties and removing the flexibility may not be a good idea.
    
    The question is not about advantage of static checker, but more about choice and stability of the new mechanism.



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214707925
  
    >>Consider this an admin creates a read-only admin role and accounts with that (such users can only do list* calls), now if they go back to static-checker all such users now become default root admin (based on account type, translation) and can call all APIs defined in commands.properties -- this is a significant security risk. Therefore, I personally don't want to put users at risk and just discourage them using the static checker.
    
    @rhtyd This is what I would ideally like to see. If the user doesn't want the dynamic roles for whatever reason he shouldn't be forced to use it. There should be a one time choice given to users if they want to use dynamic roles. If user decides to go ahead with it, do the data migration and there shouldn't be an option to come back to static checker (for the reasons you have rightly mentioned). But if user wants to continue with static checker, it should continue to work on the basis of commands.properties.
    
    The current way you have implemented it using global config is confusing. Especially when you say that simply disabling the flag won't fall back to the static checker automatically and additional stuff needs to be done to make it work.
    
    Let me know what you think?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60771928
  
    --- Diff: scripts/util/migrate-dynamicroles.py ---
    @@ -0,0 +1,134 @@
    +#!/usr/bin/python
    +# -*- coding: utf-8 -*-
    +# 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.
    +
    +import os
    +import sys
    +import uuid
    +
    +from contextlib import closing
    +from optparse import OptionParser
    +
    +try:
    +    import MySQLdb
    +except ImportError:
    +    print("MySQLdb cannot be imported, please install python-mysqldb(apt) or mysql-python(yum)")
    +    sys.exit(1)
    +
    +dryrun = False
    +
    +
    +def runSql(conn, query):
    +    if dryrun:
    +        print("Running SQL query: " + query)
    +        return
    +    with closing(conn.cursor()) as cursor:
    +        cursor.execute(query)
    +
    +
    +def migrateApiRolePermissions(apis, conn):
    +    # All allow for root admin role Admin(id:1)
    +    runSql(conn, "INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), 1, '*', 'Allow');")
    +    # Migrate rules based on commands.properties rule for ResourceAdmin(id:2), DomainAdmin(id:3), User(id:4)
    +    octetKey = {2:2, 3:4, 4:8}
    +    for role in [2, 3, 4]:
    +        for api in sorted(apis.keys()):
    +            # Ignore auth commands
    +            if api in ['login', 'logout', 'samlSso', 'samlSlo', 'listIdps', 'listAndSwitchSamlAccount', 'getSPMetadata']:
    +                continue
    +            if (octetKey[role] & int(apis[api])) > 0:
    +                runSql(conn, "INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'Allow');" % (role, api))
    +
    +
    +def main():
    +    parser = OptionParser()
    +    parser.add_option("-b", "--db", action="store", type="string", dest="db", default="cloud",
    +                        help="The name of the database, default: cloud")
    +    parser.add_option("-u", "--user", action="store", type="string", dest="user", default="cloud",
    +                        help="User name a MySQL user with privileges on cloud database")
    +    parser.add_option("-p", "--password", action="store", type="string", dest="password", default="cloud",
    +                        help="Password of a MySQL user with privileges on cloud database")
    +    parser.add_option("-H", "--host", action="store", type="string", dest="host", default="127.0.0.1",
    +                        help="Host or IP of the MySQL server")
    +    parser.add_option("-P", "--port", action="store", type="int", dest="port", default=3306,
    +                        help="Host or IP of the MySQL server")
    +    parser.add_option("-f", "--properties-file", action="store", type="string", dest="commandsfile", default="/etc/cloudstack/management/commands.properties",
    +                        help="The commands.properties file")
    +    parser.add_option("-d", "--dryrun", action="store_true", dest="dryrun", default=False,
    +                        help="Dry run and debug operations this tool will perform")
    +    (options, args) = parser.parse_args()
    +
    +    print("Apache CloudStack Role Permission Migration Tool")
    +    print("(c) Apache CloudStack Authors and the ASF, under the Apache License, Version 2.0\n")
    +
    +    global dryrun
    +    if options.dryrun:
    +        dryrun = True
    +
    +    conn = MySQLdb.connect(
    +            host=options.host,
    +            user=options.user,
    +            passwd=options.password,
    +            port=int(options.port),
    +            db=options.db)
    +
    +    if not os.path.isfile(options.commandsfile):
    +        print("Provided commands.properties cannot be accessed or does not exist, please check check permissions")
    +        sys.exit(1)
    +
    +    while True:
    +        choice = raw_input("Running this migration tool will remove any " +
    +                           "default-role rules in cloud.role_permissions. " +
    +                           "Do you want to continue? [y/N]").lower()
    +        if choice == 'y':
    +            break
    +        else:
    +            print("Aborting!")
    +            sys.exit(1)
    +
    +    # Generate API to permission octet map
    +    apiMap = {}
    +    with open(options.commandsfile) as f:
    +        for line in f.readlines():
    +            if not line or line == '' or line == '\n' or line.startswith('#'):
    +                continue
    +            name, value = line.split('=')
    +            apiMap[name.strip()] = value.strip()
    +
    +    # Rename and deprecate old commands.properties file
    +    if not dryrun:
    +        os.rename(options.commandsfile, options.commandsfile + '.deprecated')
    +    print("The commands.properties file has been deprecated and moved at: " + options.commandsfile + '.deprecated')
    +
    +    # Truncate any rules in cloud.role_permissions table
    +    runSql(conn, "DELETE FROM `cloud`.`role_permissions` WHERE `role_id` in (1,2,3,4);")
    +
    +    # Migrate rules from commands.properties to cloud.role_permissions
    +    migrateApiRolePermissions(apiMap, conn)
    +    print("Static role permissions from commands.properties have been migrated into the db")
    +
    +    # Enable dynamic role based API checker
    +    runSql(conn, "UPDATE `cloud`.`configuration` SET value='true' where name='dynamic.apichecker.enabled'")
    +    conn.commit()
    --- End diff --
    
    The commit() occurs at the very end, in case of an exception they will be printed on stdout/stderr and script will exit, if I could add a try/except but that will do the same. The way script performs migration, changes are idempotent wrt a provided commands.properties file.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215197356
  
    Rebased against latest master, please consider this for merge
    
    Performed final rounds of testing:
    - Build/maven, unit and integration tests
    - Upgrade tests from old deployed to new using packages
    - Major regression tests against before/after upgrade LDAP, SAML2 and native cloudstack accounts, listApis regression testing, along with API/CLI testing


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-218369488
  
    @swill all green now 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60357906
  
    --- Diff: engine/schema/src/org/apache/cloudstack/acl/RolePermissionVO.java ---
    @@ -0,0 +1,109 @@
    +// 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.cloudstack.acl;
    +
    +import javax.persistence.Column;
    +import javax.persistence.Entity;
    +import javax.persistence.EnumType;
    +import javax.persistence.Enumerated;
    +import javax.persistence.GeneratedValue;
    +import javax.persistence.GenerationType;
    +import javax.persistence.Id;
    +import javax.persistence.Table;
    +import java.util.UUID;
    +
    +@Entity
    +@Table(name = "role_permissions")
    +public class RolePermissionVO implements RolePermission {
    --- End diff --
    
    @koushik-das with the current static commands.properties based approach, new APIs are enabled for role by developer by making changes in commands.properties file. During installation it is seen that any pre-existing commands.properties file does not get overwritten, so admin need to enable/change API for roles manually in commands.properties file.
    
    With this feature, the API developer when adding new API will enable the API for the default role type using the authorized field in \@APICommand annotation. This is strictly to enforce default behaviour when there are no allow or deny rule for that API. The admin when upgrading to a new version to get the new APIs etc won't have to manually allow/deny them if authorized annotation is present. In general, the release notes should have list of new APIs that admins read and decide if they want to update/add/modify role permissions.
    
    I'm not sure what you mean by `Role refers RolePermissions`, as of now each role has a list of role permissions linked to it if that's what you're asking.
    
    This has been documented in the FS under the section E: https://cwiki.apache.org/confluence/display/CLOUDSTACK/Dynamic+Role+Based+API+Access+Checker+for+CloudStack


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60389152
  
    --- Diff: engine/schema/src/org/apache/cloudstack/acl/RolePermissionVO.java ---
    @@ -0,0 +1,109 @@
    +// 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.cloudstack.acl;
    +
    +import javax.persistence.Column;
    +import javax.persistence.Entity;
    +import javax.persistence.EnumType;
    +import javax.persistence.Enumerated;
    +import javax.persistence.GeneratedValue;
    +import javax.persistence.GenerationType;
    +import javax.persistence.Id;
    +import javax.persistence.Table;
    +import java.util.UUID;
    +
    +@Entity
    +@Table(name = "role_permissions")
    +public class RolePermissionVO implements RolePermission {
    --- End diff --
    
    I am not sure I understand how is back-compat going to be impacted. In the current model I don't think there is any explicit deny. In the static checker the order is command.properties override, command.properties and then the annotations. If the API is not found anywhere then its a deny by default.
    About the wildcard permissions, the same can be referenced in multiple roles.
    About the 4th point, if it is for all roles then agree but what if it is for 90% of the roles. Still there will be lot of duplication.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214430311
  
    > @rhtyd My comment regarding the test was more in the context of perf. test. In the DB for regular user I saw ~250 permissions got created. So this means iterating over all these entries twice (ALLOW and DENY) to find a match and then perform access check.
    
    The two checkPermissions calls would not cause significant overhead as they are done in memory and cost a maximum (worst case) of O(n) computation (on same machine). While making two DB calls (once to get all ALLOW rules and once to get all DENY rules) is more expensive as we do two network calls to get the data and still hit worst case O(n) computation.
    
    For a dynamic access checking system, this is a trade off and also a feature. In case of static checker we've now, rules are loaded at load-time only; any change requires restart and rules can be inconsistent across server(s).
    
    > There will be a perf. overhead due to this and user should have an option to decide whether to use static or dynamic.
    
    Users do have a choice, they can choose to not migrate to this feature. Typically, in production organizations would run multiple management servers; mgmt servers can be horizontally scaled to meet demanding usage. For example, they can can mgmt server on a separate machine (which they generally do) and tune their databases to accept up to 50k requests (or qps), optimize innodb settings (buffer pool size to 60% of memory etc.) and in db.properties increase num. of db connections from 250 to 1000.
    
    > Also if the user finds some issues/bugs later during their testing there should be a fallback option.
    
    There is a way to revert back to using static checker as I explained earlier, (1) switch off the global settings and (2) put back a commands.properties file in class-path usually at /etc/cloudstack/management; it's just that we don't want users to do it easily. Consider this an admin creates a read-only admin role and accounts with that (such users can only do list* calls), now if they go back to static-checker all such users now become default root admin (based on account type, translation) and can call all APIs defined in commands.properties -- this is a significant security risk. Therefore, I personally don't want to put users at risk and just discourage them using the static checker.
    
    > Regarding upgrade implications, I went through the docs/FS but some things are still confusing. If existing user can continue using commands.properties then what happens to the new APIs that gets added.
    
    In case you're not aware, with the current system each time a user upgrade they have to edit/add new rules to commands.properties by hand. Upgrading using packages does not update commands.properties file; it would create a .rpmnew/rpmsave file for example. In case of multiple mgmt server, you have to do this on all server(s) and restart all of them. While in case of dynamic roles based checker, you don't need to restart mgmt server at all (even during migration). I'm not sure why you say the static checker way is flexible, on the contrary I think it is not and a pain point of a lot of people.
    
    > If the argument is that the permission can be put in as an annotation in code for new APIs then that removes the flexibility of the earlier mechanism (there is no way to modify the default in code).
    
    This has existed for so long, but  not popular among API writers. Even before this feature, there have been few APIs using the annotation; the static checker also uses annotation as a fallback (i.e. not something I've introduced).
    
    There is a way to modify default behavior by adding authorized field in @APIParam. See the new APIs implementation for example. I've added a section in the FS on how new APIs should be written if they need to be enabled by default for a role type; alternatively the release notes should properly document new APIs and leave the choice of allowing/denying those APIs to (custom) roles.
    
    > We don't know how people are customising commands.properties and removing the flexibility may not be a good idea.
    
    On the contrary, we've giving flexibility to people. It might make sense to enable certain features for all role types or a subset of them. We have deny rules (with API and wildcards supported) in case they want to override the default. Consider this, you wrote an API and enabled for users; the system admin can explicitly add allow rules and add a \* deny rule that is to say deny all (if not allowed) and the dynamic roles system would not consider default rules in annotations at all.
    
    > The question is not about advantage of static checker, but more about choice and stability of the new mechanism.
    
    There is both choice and stability. We've tried our best to make this feature a drop in replacement that is strictly backward compatibility, with good integration tests and coverage on critical pieces. In fact, the integration tests run with Travis to ensure this becomes one of the critical smoke tests and aims to provide nearly 100% end-to-end coverage.
    
    If you've any code specific reservation or suggestions on improving it, I'll be happy to fix them.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60744675
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    +        return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10))
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_list(self):
    +        """
    +            Tests that default four roles exist
    +        """
    +        roleTypes = {1: "Admin", 2: "ResourceAdmin", 3: "DomainAdmin", 4: "User"}
    +        for idx in range(1,5):
    +            list_roles = Role.list(self.apiclient, id=idx)
    +            self.assertEqual(
    +                isinstance(list_roles, list),
    +                True,
    +                "List Roles response was not a valid list"
    +            )
    +            self.assertEqual(
    +                len(list_roles),
    +                1,
    +                "List Roles response size was not 1"
    +            )
    +            self.assertEqual(
    +                list_roles[0].type,
    +                roleTypes[idx],
    +                msg="Default role type differs from expectation"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_create(self):
    +        """
    +            Tests normal lifecycle operations for roles
    +        """
    +        # Reuse self.role created in setUp()
    +        try:
    +            role = Role.create(
    +                self.apiclient,
    +                self.testdata["role"]
    +            )
    +            self.fail("An exception was expected when creating duplicate roles")
    +        except CloudstackAPIException: pass
    +
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_roles, list),
    +            True,
    +            "List Roles response was not a valid list"
    +        )
    +        self.assertEqual(
    +            len(list_roles),
    +            1,
    +            "List Roles response size was not 1"
    +        )
    +        self.assertEqual(
    +            list_roles[0].name,
    +            self.testdata["role"]["name"],
    +            msg="Role name does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_roles[0].type,
    +            self.testdata["role"]["type"],
    +            msg="Role type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +        self.assertEqual(
    +            update_role.type,
    +            'Admin',
    +            msg="Role type does not match updated role type"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update_role_inuse(self):
    +        """
    +            Tests role update when role is in use by an account
    +        """
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        try:
    +            self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +            self.fail("Updation of role type is not allowed when role is in use")
    +        except CloudstackAPIException: pass
    +
    +        self.role.update(self.apiclient, name=new_role_name)
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_delete(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        self.role.delete(self.apiclient)
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            list_roles,
    +            None,
    +            "List Roles response should be empty"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_inuse_deletion(self):
    +        """
    +            Test to ensure role in use cannot be deleted
    +        """
    +        try:
    +            self.role.delete(self.apiclient)
    +            self.fail("Role with any account should not be allowed to be deleted")
    +        except CloudstackAPIException: pass
    --- End diff --
    
    Is there any value asserting on the contents of the error message?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59514254
  
    --- Diff: api/test/org/apache/cloudstack/acl/RoleTypeTest.java ---
    @@ -0,0 +1,92 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.user.Account;
    +import org.junit.Assert;
    +import org.junit.Test;
    +import org.mockito.Mockito;
    +
    +import java.util.Arrays;
    +
    +public class RoleTypeTest {
    +
    +    @Test
    +    public void testRoleTypeFromString() {
    +        Assert.assertEquals(RoleType.fromString(null), null);
    +        Assert.assertEquals(RoleType.fromString(""), null);
    +        Assert.assertEquals(RoleType.fromString("admin"), null);
    +        Assert.assertEquals(RoleType.fromString("12345%^&*"), null);
    +        for (RoleType roleType : RoleType.values()) {
    +            Assert.assertEquals(RoleType.fromString(roleType.name()), roleType);
    --- End diff --
    
    never mind, confused Role with RoleType


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215290621
  
    
    
    ### CI RESULTS
    
    ```
    Tests Run: 100
      Skipped: 1
       Failed: 0
       Errors: 0
    ```
    
    
    
    **Associated Uploads**
    
    **`/tmp/MarvinLogs/DeployDataCenter__Apr_27_2016_22_04_24_8LX15C:`**
    * [dc_entries.obj](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/DeployDataCenter__Apr_27_2016_22_04_24_8LX15C/dc_entries.obj)
    * [failed_plus_exceptions.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/DeployDataCenter__Apr_27_2016_22_04_24_8LX15C/failed_plus_exceptions.txt)
    * [runinfo.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/DeployDataCenter__Apr_27_2016_22_04_24_8LX15C/runinfo.txt)
    
    **`/tmp/MarvinLogs/test_network_8F05KM:`**
    * [failed_plus_exceptions.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_network_8F05KM/failed_plus_exceptions.txt)
    * [results.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_network_8F05KM/results.txt)
    * [runinfo.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_network_8F05KM/runinfo.txt)
    
    **`/tmp/MarvinLogs/test_staticroles_E7WUSK:`**
    * [failed_plus_exceptions.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_staticroles_E7WUSK/failed_plus_exceptions.txt)
    * [results.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_staticroles_E7WUSK/results.txt)
    * [runinfo.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_staticroles_E7WUSK/runinfo.txt)
    
    
    Uploads will be available until `2016-06-28 02:00:00 +0200 CEST`
    
    *Comment created by [`upr comment`](https://github.com/cloudops/upr).*



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59797686
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/ListRolePermissionsCmd.java ---
    @@ -0,0 +1,104 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.exception.InsufficientCapacityException;
    +import com.cloud.exception.ResourceUnavailableException;
    +import com.cloud.user.Account;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RolePermission;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.ListResponse;
    +import org.apache.cloudstack.api.response.RolePermissionResponse;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +
    +@APICommand(name = ListRolePermissionsCmd.APINAME, description = "Lists role permissions", responseObject = RolePermissionResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class ListRolePermissionsCmd extends BaseCmd {
    +    public static final String APINAME = "listRolePermissions";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.ROLE_ID, type = CommandType.UUID, entityType = RoleResponse.class, description = "ID of the role")
    +    private Long roleId;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public Long getRoleId() {
    +        return roleId;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException {
    +        if (getRoleId() != null && getRoleId() < 1L) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    +        List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(getRoleId());
    +
    +        ListResponse<RolePermissionResponse> response = new ListResponse<>();
    +        List<RolePermissionResponse> rolePermissionResponses = new ArrayList<>();
    +        for (RolePermission rolePermission : rolePermissions) {
    +            Role role = roleService.findRole(rolePermission.getRoleId());
    --- End diff --
    
    Am I correct in understanding that all the permissions in ``rolePerimssions`` will be for ``roleId``?  If so, why do we retrieve the same role for every permission evaluated?  Why not retrieve the role once between the for loop?  A step further, why not model ``RolePermission`` to have an association with ``Role`` and retrieve and associate it in ``roleService.findAllPermissionsBy``?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59787197
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/account/CreateAccountCmd.java ---
    @@ -196,5 +204,8 @@ private void validateParams() {
             if(StringUtils.isEmpty(getPassword())) {
                 throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty passwords are not allowed");
             }
    +        if (getAccountType() == null && getRoleId() == null) {
    --- End diff --
    
    Consider splitting these checks to give a more precise error message to the end user.  Also, shouldn't check that ``roleId`` is greater than ``0L``?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-209347260
  
    @DaanHoogland thanks, I'll fix them


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-212852849
  
    @jburwell I've fixed outstanding issues, including a framework level API arg validation (and removed such validations across cmd implementations)
    @koushik-das let me know if you've more questions
    
    Thanks all for the review, I believe all of the outstanding issues have been solved and fixed. /cc @swill 
    
    I've executed another test round locally and it looks good to me 
    ==== Marvin Init Started ====
    
    === Marvin Parse Config Successful ===
    
    === Marvin Setting TestData Successful===
    
    ==== Log Folder Path: /tmp//MarvinLogs//Apr_21_2016_16_16_18_FM3SK5. All logs will be available here ====
    
    === Marvin Init Logging Successful===
    
    ==== Marvin Init Successful ====
    === TestName: test_default_role_deletion | Status : SUCCESS ===
    
    === TestName: test_role_account_acls | Status : SUCCESS ===
    
    === TestName: test_role_account_acls_multiple_mgmt_servers | Status : SUCCESS ===
    
    === TestName: test_role_inuse_deletion | Status : SUCCESS ===
    
    === TestName: test_role_lifecycle_create | Status : SUCCESS ===
    
    === TestName: test_role_lifecycle_delete | Status : SUCCESS ===
    
    === TestName: test_role_lifecycle_list | Status : SUCCESS ===
    
    === TestName: test_role_lifecycle_update | Status : SUCCESS ===
    
    === TestName: test_role_lifecycle_update_role_inuse | Status : SUCCESS ===
    
    === TestName: test_rolepermission_lifecycle_create | Status : SUCCESS ===
    
    === TestName: test_rolepermission_lifecycle_delete | Status : SUCCESS ===
    
    === TestName: test_rolepermission_lifecycle_list | Status : SUCCESS ===
    
    === TestName: test_rolepermission_lifecycle_update | Status : SUCCESS ===
    
    === TestName: test_rolepermission_lifecycle_update_invalid_rule | Status : SUCCESS ===



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214276787
  
    @koushik-das Yes, all tests run as a regular user too. See the integration test, we're using user api clients (search self.getUserApiClient) to perform tests -- i.e. tests are not run all as root admin only. What you're asking is already covered, they are also run by Travis.
    
    I'm sorry if the discussion confused you, please re-read FS again but let me try to explain below as well;
    
    By default, we don't want to encourage new users to use static checker which is why dynamic-checker is enabled for developers/new-users. For this reason the commands.properties.in file in codebase has been deprecated. In packaging too, we're not including commands.properties file.
    
    For existing deployments, we are *NOT* forcing users to migrate to the dynamic roles feature and their existing commands.properties file won't be renamed or removed during upgrade. Though, the upgrade path will add dynamic-role specific tables/schema and default roles. There is an upgrade/migrate script for such users who can migrate in future at their wish, the script will read rules from commands.properties file and put them in DB.
    
    Please read the admin docs too if they help you understand the process:
    https://github.com/apache/cloudstack-docs-admin/pull/37
    
    
    Now, once a users is already using dynamic checker (fresh or migrated at a later stage), we don't want them to be easily able to migrate back to static checker as allowing admin to do that with a global setting switch is a security issue (sorry being pessimistic here). Therefore, we do two checks to evaluate if dynamic roles is allowed:
    - check if the global setting says that dynamic roles is enabled
    - check that commands.properties does not exist
    The reverse is true for static checker, see the isEnabled()/isDisabled method in the checker implementation.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60737450
  
    --- Diff: plugins/user-authenticators/ldap/src/org/apache/cloudstack/api/command/LdapImportUsersCmd.java ---
    @@ -131,7 +135,9 @@ private void createCloudstackUserAccount(LdapUser user, String accountName, Doma
         @Override
         public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException,
             ResourceAllocationException, NetworkRuleConflictException {
    -
    +        if (getAccountType() == null && getRoleId() == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Both account type and role ID are not provided");
    --- End diff --
    
    Why not mark these two parameters as required and let the framework perform the validation?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220238585
  
    @anshul1886 if you need help join this GTM now: https://app.gotomeeting.com/?meetingId=981540549 (or use the meeting id for a desktop client). I've personally tested master and I think the issues you're presenting are your specific env related.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-217774436
  
    @swill thanks, let's merge this


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220005496
  
    @anshul1886 okay, let's debug;
    - After you mvn re-built cleanly, did you run mvn deploydb?
    - If yes, what is the output of: select * from configuration where name='dynamic.apichecker.enabled'\G;
    For a fresh/clean install, the `value` should be 'true'.
    - Do you see any rules in cloud.role_permissions table for admin user (role_id=1). Default root admins (or account created using the default root admin role) are allowed all APIs. Are you using CloudStack as a non-root admin?
    - Check that there is no `commands.properties` file in the classpath for dynamic-roles to work. None of the apichecker will work if you've a commands.properties file in classpath but have dynamic.apichecker.enabled set to true. Try git clean -fdx to get rid of non-tracked files, or do a `find` and remove commands.properties file.
    - Check role_id column in cloud.accounts table, it should be `1` for root admin accounts. If it's NULL, something went wrong with the upgrade path.
    - Check errors on why the APIs are failing are you seeing, open Chrome dev-tools etc and share the results. Also check api/mgmt server logs.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by anshul1886 <gi...@git.apache.org>.
Github user anshul1886 commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220236230
  
    <img width="1278" alt="screen shot 2016-05-19 at 11 34 36 am" src="https://cloud.githubusercontent.com/assets/1712815/15384187/2e8d4aa2-1db6-11e6-9358-659ff6f6df3f.png">
    <img width="1305" alt="screen shot 2016-05-19 at 11 35 06 am" src="https://cloud.githubusercontent.com/assets/1712815/15384191/2eeeb7ec-1db6-11e6-8bc4-d68a8c10421d.png">
    <img width="1439" alt="screen shot 2016-05-19 at 11 36 20 am" src="https://cloud.githubusercontent.com/assets/1712815/15384189/2ee75aec-1db6-11e6-9c6a-b1c91f9bf6e9.png">
    <img width="1279" alt="screen shot 2016-05-19 at 11 36 58 am" src="https://cloud.githubusercontent.com/assets/1712815/15384190/2eeb7ae6-1db6-11e6-868d-37c15e214d16.png">
    
    @rhtyd Same results on master. I have not done anything in setup.
    
    My dev box is ubuntu 12.04


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59856523
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/ListRolePermissionsCmd.java ---
    @@ -0,0 +1,104 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.exception.InsufficientCapacityException;
    +import com.cloud.exception.ResourceUnavailableException;
    +import com.cloud.user.Account;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RolePermission;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.ListResponse;
    +import org.apache.cloudstack.api.response.RolePermissionResponse;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +
    +@APICommand(name = ListRolePermissionsCmd.APINAME, description = "Lists role permissions", responseObject = RolePermissionResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class ListRolePermissionsCmd extends BaseCmd {
    +    public static final String APINAME = "listRolePermissions";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.ROLE_ID, type = CommandType.UUID, entityType = RoleResponse.class, description = "ID of the role")
    +    private Long roleId;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public Long getRoleId() {
    +        return roleId;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException {
    +        if (getRoleId() != null && getRoleId() < 1L) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    +        List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(getRoleId());
    +
    +        ListResponse<RolePermissionResponse> response = new ListResponse<>();
    +        List<RolePermissionResponse> rolePermissionResponses = new ArrayList<>();
    +        for (RolePermission rolePermission : rolePermissions) {
    +            Role role = roleService.findRole(rolePermission.getRoleId());
    --- End diff --
    
    I was trying to avoid a table join and Join Daos/VOs, in most cases you won't be running this API without providing a role Id. I've fixed it for average case.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by anshul1886 <gi...@git.apache.org>.
Github user anshul1886 commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-219944062
  
    @swill @rhtyd Some APIs are giving permission exceptions. They can be seen by just logging into UI. I have added the commands.properties to overcome these exceptions in my setup.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220090972
  
    I agree.  But I have not tested the UI at all.  Has your team been testing through the UI as well as through the API?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220385286
  
    Thanks guys for getting that sorted out.  Is the problems that @anshul1886 ran into something that others could potentially run into?  Would it be useful to document the problem and the solution so others can benefit from it if they run into it?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59855472
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/account/CreateAccountCmd.java ---
    @@ -196,5 +204,8 @@ private void validateParams() {
             if(StringUtils.isEmpty(getPassword())) {
                 throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty passwords are not allowed");
             }
    +        if (getAccountType() == null && getRoleId() == null) {
    --- End diff --
    
    Fixed the roleId < 1L issue.
    No, here the check is for maintaining backward compatibility. The accounttype used to be a 'required' param, but now it's optional, and so is 'roleid'; this check is to ensure that at least one of the two are provided


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60573629
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,170 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import org.apache.log4j.Logger;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    protected static final Logger LOGGER = Logger.getLogger(DynamicRoleBasedAPIAccessChecker.class);
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null || permissions.isEmpty() || Strings.isNullOrEmpty(commandName)) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            try {
    +                final Rule rule = new Rule(permission.getRule());
    +                if (rule.matches(commandName)) {
    +                    return true;
    +                }
    +            } catch (InvalidParameterValueException e) {
    +                LOGGER.warn("Invalid rule permission, please fix id=" + permission.getId() + " rule=" + permission.getRule());
    +                continue;
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    +            return true;
    +        }
    +
    +        // Check for deny rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.DENY, commandName)) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Check annotations
    +        if (annotationRoleBasedApisMap.get(accountRole.getRoleType()) != null
    +                && annotationRoleBasedApisMap.get(accountRole.getRoleType()).contains(commandName)) {
    +            return true;
    +        }
    +
    +        denyApiAccess(commandName);
    +        return false;
    +    }
    +
    +    public void addApiToRoleBasedAnnotationsMap(final RoleType roleType, final String commandName) {
    +        if (roleType == null || Strings.isNullOrEmpty(commandName)) {
    +            return;
    +        }
    +        final Set<String> commands = annotationRoleBasedApisMap.get(roleType);
    +        if (commands != null && !commands.contains(commandName)) {
    +            commands.add(commandName);
    +        }
    +    }
    +
    +    @Override
    +    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
    +        super.configure(name, params);
    +        return true;
    +    }
    +
    +    @Override
    +    public boolean start() {
    +        for (PluggableService service : services) {
    +            for (Class<?> clz : service.getCommands()) {
    +                APICommand command = clz.getAnnotation(APICommand.class);
    +                for (RoleType role : command.authorized()) {
    +                    addApiToRoleBasedAnnotationsMap(role, command.name());
    --- End diff --
    
    @koushik-das plugins that are not in the source tree may be using these annotations.  Therefore, removing it may break end-user customizations.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59512724
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/CreateRoleCmd.java ---
    @@ -0,0 +1,104 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.user.Account;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +import org.apache.cloudstack.context.CallContext;
    +
    +@APICommand(name = CreateRoleCmd.APINAME, description = "Creates a role", responseObject = RoleResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class CreateRoleCmd extends BaseCmd {
    +    public static final String APINAME = "createRole";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "creates a role with this unique name")
    +    private String roleName;
    +
    +    @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "The type of the role, valid options are: Admin, ResourceAdmin, DomainAdmin, User")
    +    private String roleType;
    +
    +    @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the role")
    +    private String roleDescription;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public String getRoleName() {
    +        return roleName;
    +    }
    +
    +    public RoleType getRoleType() {
    +        return RoleType.fromString(roleType);
    +    }
    +
    +    public String getRoleDescription() {
    +        return roleDescription;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() {
    +        if (Strings.isNullOrEmpty(getRoleName())) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty role name provided");
    +        }
    +        if (getRoleType() == null || getRoleType() == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
    +        }
    +        CallContext.current().setEventDetails("Role: " + getRoleName() + ", type:" + getRoleType() + ", description: " + getRoleDescription());
    +        Role role = roleService.createRole(getRoleName(), getRoleType(), getRoleDescription());
    +        if (role == null) {
    +            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create role");
    +        }
    +        RoleResponse response = new RoleResponse();
    +        response.setId(role.getUuid());
    +        response.setRoleName(role.getName());
    +        response.setRoleType(role.getRoleType());
    +        response.setResponseName(getCommandName());
    +        response.setObjectName("role");
    +        setResponseObject(response);
    +    }
    --- End diff --
    
    in other api command classes this is done in the ApiResponseHelper. Makes sense not to do that but for readibility maybe factor it out in a private method?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60585031
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,170 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import org.apache.log4j.Logger;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    protected static final Logger LOGGER = Logger.getLogger(DynamicRoleBasedAPIAccessChecker.class);
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null || permissions.isEmpty() || Strings.isNullOrEmpty(commandName)) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            try {
    +                final Rule rule = new Rule(permission.getRule());
    +                if (rule.matches(commandName)) {
    +                    return true;
    +                }
    +            } catch (InvalidParameterValueException e) {
    +                LOGGER.warn("Invalid rule permission, please fix id=" + permission.getId() + " rule=" + permission.getRule());
    +                continue;
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    +            return true;
    +        }
    +
    +        // Check for deny rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.DENY, commandName)) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Check annotations
    +        if (annotationRoleBasedApisMap.get(accountRole.getRoleType()) != null
    +                && annotationRoleBasedApisMap.get(accountRole.getRoleType()).contains(commandName)) {
    +            return true;
    +        }
    +
    +        denyApiAccess(commandName);
    +        return false;
    +    }
    +
    +    public void addApiToRoleBasedAnnotationsMap(final RoleType roleType, final String commandName) {
    +        if (roleType == null || Strings.isNullOrEmpty(commandName)) {
    +            return;
    +        }
    +        final Set<String> commands = annotationRoleBasedApisMap.get(roleType);
    +        if (commands != null && !commands.contains(commandName)) {
    +            commands.add(commandName);
    +        }
    +    }
    +
    +    @Override
    +    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
    +        super.configure(name, params);
    +        return true;
    +    }
    +
    +    @Override
    +    public boolean start() {
    +        for (PluggableService service : services) {
    +            for (Class<?> clz : service.getCommands()) {
    +                APICommand command = clz.getAnnotation(APICommand.class);
    +                for (RoleType role : command.authorized()) {
    +                    addApiToRoleBasedAnnotationsMap(role, command.name());
    --- End diff --
    
    @koushik-das there are indeed APIs in current master using these annotations (see baremetal related and maybe others). The dynamic rbac and out-of-band PRs are also using these annotations. We need these annotations so new APIs can be automatically for role types if necessary.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60358060
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            final String rule = permission.getRule();
    +            if (rule.contains("*")) {
    +                if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                    return true;
    +                }
    +            } else {
    +                if (commandName.equals(rule)) {
    +                    return true;
    +                }
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    --- End diff --
    
    For a role if there are no permissions whats the default policy - allow or deny? Based on the default you either need allow or deny and not both. Or is there a provision to select the default policy for a role and in that case role permissions only have exceptions to the default policy.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59523371
  
    --- Diff: engine/schema/src/com/cloud/user/AccountVO.java ---
    @@ -83,8 +86,16 @@ public AccountVO(String accountName, long domainId, String networkDomain, short
             this.domainId = domainId;
             this.networkDomain = networkDomain;
             this.type = type;
    -        state = State.enabled;
    +        this.state = State.enabled;
             this.uuid = uuid;
    +        this.roleId = RoleType.getRoleByAccountType(null, type);
    +    }
    +
    +    public AccountVO(String accountName, long domainId, String networkDomain, short type, Long roleId, String uuid) {
    +        this(accountName, domainId, networkDomain, type, uuid);
    --- End diff --
    
    copy that


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60732532
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,138 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.warn("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    --- End diff --
    
    ``close`` is unnecessary since ``updateStatement`` is in the surrounding try with resources block.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-209326684
  
    One blocker found so far, some license headers missing.
    Otherwise looks good, I wish I could say i had completely scrutinised this. It is a big chunk. I have touched everything and I will do further review while testing.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by borisstoyanov <gi...@git.apache.org>.
Github user borisstoyanov commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214724962
  
    LGTM. :+1:


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59858602
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    +
    +                } catch (SQLException e) {
    +                    s_logger.error("Failed to update cloud.account role_id for account id:" + accountId + " with exception: " + e.getMessage());
    +                    throw new CloudRuntimeException("Exception while updating cloud.account role_id", e);
    +                }
    +            }
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Exception while migrating existing account table's role_id column to a role based on account type", e);
    +        }
    +        s_logger.debug("Done migrating existing accounts to use one of default roles based on account type");
    +    }
    +
    +    private void setupRolesAndPermissionsForDynamicRBAC(final Connection conn) {
    +        try ( PreparedStatement selectStatement = conn.prepareStatement("SELECT * FROM `cloud`.`roles`");
    +              ResultSet resultSet = selectStatement.executeQuery()) {
    +            if (resultSet != null && resultSet.next()) {
    +                resultSet.close();
    +                if (s_logger.isDebugEnabled()) {
    +                    s_logger.debug("Found existing roles. Skipping migration of commands.properties to dynamic roles table.");
    +                }
    +                return;
    +            }
    +        } catch (SQLException e) {
    +            s_logger.error("Unable to find existing roles, if you need to add default roles please add them manually. Giving up!");
    --- End diff --
    
    fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60369930
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    --- End diff --
    
    This is just a helper method, doing in-memory comparison/looping is faster than making two DB requests and provided there won't be more than 500 items returned so I just used a single method in the checkAccess() to get all rules and check for allow first, then deny, then annotations and finally deny/fail. Suggest if we still want two db calls?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60439732
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,170 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import org.apache.log4j.Logger;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    protected static final Logger LOGGER = Logger.getLogger(DynamicRoleBasedAPIAccessChecker.class);
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null || permissions.isEmpty() || Strings.isNullOrEmpty(commandName)) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            try {
    +                final Rule rule = new Rule(permission.getRule());
    +                if (rule.matches(commandName)) {
    +                    return true;
    +                }
    +            } catch (InvalidParameterValueException e) {
    +                LOGGER.warn("Invalid rule permission, please fix id=" + permission.getId() + " rule=" + permission.getRule());
    +                continue;
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    +            return true;
    +        }
    +
    +        // Check for deny rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.DENY, commandName)) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Check annotations
    +        if (annotationRoleBasedApisMap.get(accountRole.getRoleType()) != null
    +                && annotationRoleBasedApisMap.get(accountRole.getRoleType()).contains(commandName)) {
    +            return true;
    +        }
    +
    +        denyApiAccess(commandName);
    +        return false;
    +    }
    +
    +    public void addApiToRoleBasedAnnotationsMap(final RoleType roleType, final String commandName) {
    +        if (roleType == null || Strings.isNullOrEmpty(commandName)) {
    +            return;
    +        }
    +        final Set<String> commands = annotationRoleBasedApisMap.get(roleType);
    +        if (commands != null && !commands.contains(commandName)) {
    +            commands.add(commandName);
    +        }
    +    }
    +
    +    @Override
    +    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
    +        super.configure(name, params);
    +        return true;
    +    }
    +
    +    @Override
    +    public boolean start() {
    +        for (PluggableService service : services) {
    +            for (Class<?> clz : service.getCommands()) {
    +                APICommand command = clz.getAnnotation(APICommand.class);
    +                for (RoleType role : command.authorized()) {
    +                    addApiToRoleBasedAnnotationsMap(role, command.name());
    +                }
    +            }
    +        }
    +        return super.start();
    +    }
    +
    +    public List<PluggableService> getServices() {
    +        return services;
    +    }
    +
    +    @Inject
    +    public void setServices(List<PluggableService> services) {
    +        this.services = services;
    +    }
    --- End diff --
    
    @koushik-das (1) when this class in instantiated, spring injects a list of pluggable services here; pluggable services have a public method that returns list of APICommand classes


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-218357625
  
    @swill I've fixed the issues, here are the changes:
    [changes.diff.txt](https://github.com/apache/cloudstack/files/258551/changes.diff.txt)
    
    The conflicts were in travis, debian/control and the schema files only. If Travis is green, we can merge this without need to re-run the CI.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-217343619
  
    @swill rebased against latest master and pushed
    @borisstoyanov can you help test this one last time, I think all major issues including ordering related issues have been fixed, thanks


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60772392
  
    --- Diff: server/src/com/cloud/api/dispatch/ParamProcessWorker.java ---
    @@ -92,6 +94,55 @@ public void handle(final DispatchTask task) {
             processParameters(task.getCmd(), task.getParams());
         }
     
    +    private void validateNonEmptyString(final Object param, final String argName) {
    +        if (param == null || Strings.isNullOrEmpty(param.toString())) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Empty or null value provided for API arg: %s", argName));
    +        }
    +    }
    +
    +    private void validateNaturalNumber(final Object param, final String argName) {
    +        Long value = null;
    +        if (param != null && param instanceof Long) {
    +            value = (Long) param;
    +        } else if (param != null) {
    +            value = Long.valueOf(param.toString());
    +        }
    +        if (value == null || value < 1L) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Invalid value provided for API arg: %s", argName));
    +        }
    +    }
    +
    +    private void validateField(final Object paramObj, final Parameter annotation) throws ServerApiException {
    +        if (annotation == null) {
    +            return;
    +        }
    +        final String argName = annotation.name();
    +        for (final ApiArgValidator validator : annotation.validations()) {
    +            if (validator == null) {
    +                continue;
    +            }
    +            switch (validator) {
    +                case NotNullOrEmpty:
    +                    switch (annotation.type()) {
    +                        case UUID:
    +                        case STRING:
    +                            validateNonEmptyString(paramObj, argName);
    +                            break;
    +                    }
    +                    break;
    +                case PositiveNumber:
    +                    switch (annotation.type()) {
    +                        case SHORT:
    +                        case INTEGER:
    +                        case LONG:
    +                            validateNaturalNumber(paramObj, argName);
    +                            break;
    +                    }
    --- End diff --
    
    annotation fields need an exact value, so it could either have a list of enum or classes; using a list of classes would make implementation complex and creation of several classes for each new validator. We also needed to know what the annotation type is to only execute operation based on type, for example PositiveNumber validator on a  string or boolean field/arg type won't make sense. It was easiest in terms of maintainability and implementation to pass in enum, and handle them here like this. I'm not sure the best way of doing this, advise if there are better ways of doing validation?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60368615
  
    --- Diff: engine/schema/src/org/apache/cloudstack/acl/RolePermissionVO.java ---
    @@ -0,0 +1,109 @@
    +// 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.cloudstack.acl;
    +
    +import javax.persistence.Column;
    +import javax.persistence.Entity;
    +import javax.persistence.EnumType;
    +import javax.persistence.Enumerated;
    +import javax.persistence.GeneratedValue;
    +import javax.persistence.GenerationType;
    +import javax.persistence.Id;
    +import javax.persistence.Table;
    +import java.util.UUID;
    +
    +@Entity
    +@Table(name = "role_permissions")
    +public class RolePermissionVO implements RolePermission {
    --- End diff --
    
    @koushik-das okay I got what you're saying, but it's not possible. Also see we've to be backward compatible with the static checker, so (1) the order of processing of rules need to be similar to the static-checker which is first allow rules then deny checks, then finally annotation and lastly deny all, (2) rules can be both apiname or wildcards such as list\* therefore we cannot have the references reversed or what you're suggesting, (3) the model promotes explicit rules than implicit declarations for security and various use-case (so we don't assume anything), (4) if we want an API like deployVM to be available for all or some role types we can use the authorized field in the API annotation (for example using authorized={RoleType.Admin, RoleType.User ... etc} will enable this API for all when no explicit rules are set by the admin). 
    
    I've thought this through but this is the best model we can have trading feature use, functionality and security. Provided this, do you have suggestion if we can improve this.?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59788370
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/CreateRoleCmd.java ---
    @@ -0,0 +1,104 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.user.Account;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +import org.apache.cloudstack.context.CallContext;
    +
    +@APICommand(name = CreateRoleCmd.APINAME, description = "Creates a role", responseObject = RoleResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class CreateRoleCmd extends BaseCmd {
    +    public static final String APINAME = "createRole";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "creates a role with this unique name")
    +    private String roleName;
    +
    +    @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "The type of the role, valid options are: Admin, ResourceAdmin, DomainAdmin, User")
    +    private String roleType;
    +
    +    @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the role")
    +    private String roleDescription;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public String getRoleName() {
    +        return roleName;
    +    }
    +
    +    public RoleType getRoleType() {
    +        return RoleType.fromString(roleType);
    +    }
    +
    +    public String getRoleDescription() {
    +        return roleDescription;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() {
    +        if (Strings.isNullOrEmpty(getRoleName())) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty role name provided");
    +        }
    +        if (getRoleType() == null || getRoleType() == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
    +        }
    --- End diff --
    
    Consider extracting this validation logic to a private method to make things more readable.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59852956
  
    --- Diff: api/test/org/apache/cloudstack/acl/RuleTest.java ---
    @@ -0,0 +1,39 @@
    +package org.apache.cloudstack.acl;
    --- End diff --
    
    fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60737225
  
    --- Diff: plugins/user-authenticators/ldap/src/org/apache/cloudstack/api/command/LdapCreateAccountCmd.java ---
    @@ -119,6 +131,9 @@ private Long getDomainId() {
     
         @Override
         public void execute() throws ServerApiException {
    +        if (getAccountType() == null && getRoleId() == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Both account type and role ID are not provided");
    --- End diff --
    
    Why not mark these two parameters as required and let the framework perform the validation?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60880195
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    +        return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10))
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_list(self):
    +        """
    +            Tests that default four roles exist
    +        """
    +        roleTypes = {1: "Admin", 2: "ResourceAdmin", 3: "DomainAdmin", 4: "User"}
    +        for idx in range(1,5):
    +            list_roles = Role.list(self.apiclient, id=idx)
    +            self.assertEqual(
    +                isinstance(list_roles, list),
    +                True,
    +                "List Roles response was not a valid list"
    +            )
    +            self.assertEqual(
    +                len(list_roles),
    +                1,
    +                "List Roles response size was not 1"
    +            )
    +            self.assertEqual(
    +                list_roles[0].type,
    +                roleTypes[idx],
    +                msg="Default role type differs from expectation"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_create(self):
    +        """
    +            Tests normal lifecycle operations for roles
    +        """
    +        # Reuse self.role created in setUp()
    +        try:
    +            role = Role.create(
    +                self.apiclient,
    +                self.testdata["role"]
    +            )
    +            self.fail("An exception was expected when creating duplicate roles")
    +        except CloudstackAPIException: pass
    +
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_roles, list),
    +            True,
    +            "List Roles response was not a valid list"
    +        )
    +        self.assertEqual(
    +            len(list_roles),
    +            1,
    +            "List Roles response size was not 1"
    +        )
    +        self.assertEqual(
    +            list_roles[0].name,
    +            self.testdata["role"]["name"],
    +            msg="Role name does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_roles[0].type,
    +            self.testdata["role"]["type"],
    +            msg="Role type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +        self.assertEqual(
    +            update_role.type,
    +            'Admin',
    +            msg="Role type does not match updated role type"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update_role_inuse(self):
    +        """
    +            Tests role update when role is in use by an account
    +        """
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        try:
    +            self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +            self.fail("Updation of role type is not allowed when role is in use")
    +        except CloudstackAPIException: pass
    +
    +        self.role.update(self.apiclient, name=new_role_name)
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_delete(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        self.role.delete(self.apiclient)
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            list_roles,
    +            None,
    +            "List Roles response should be empty"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_inuse_deletion(self):
    +        """
    +            Test to ensure role in use cannot be deleted
    +        """
    +        try:
    +            self.role.delete(self.apiclient)
    +            self.fail("Role with any account should not be allowed to be deleted")
    +        except CloudstackAPIException: pass
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_default_role_deletion(self):
    +        """
    +            Test to ensure 4 default roles cannot be deleted
    +        """
    +        for idx in range(1,5):
    +            cmd = deleteRole.deleteRoleCmd()
    +            cmd.id = idx
    +            try:
    +                self.apiclient.deleteRole(cmd)
    +                self.fail("Default role got deleted with id: " + idx)
    +            except CloudstackAPIException: pass
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_list(self):
    +        """
    +            Tests listing of default role's permission
    +        """
    +        for idx in range(1,5):
    +            list_rolepermissions = RolePermission.list(self.apiclient, roleid=idx)
    +            self.assertEqual(
    +                isinstance(list_rolepermissions, list),
    +                True,
    +                "List rolepermissions response was not a valid list"
    +            )
    +            self.assertTrue(
    +                len(list_rolepermissions) > 0,
    +                "List rolepermissions response was empty"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_create(self):
    +        """
    +            Tests creation of role permission
    +        """
    +        # Reuse self.rolepermission created in setUp()
    +        try:
    +            rolepermission = RolePermission.create(
    +                self.apiclient,
    +                self.testdata["rolepermission"]
    +            )
    +            self.fail("An exception was expected when creating duplicate role permissions")
    +        except CloudstackAPIException: pass
    --- End diff --
    
    We can do that but since we've catching any ACS server api exception caught by CloudStackAPIException, we know it's ACS specific API exception and not any HTTP exception.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216488843
  
    @jburwell I did not get the question, is it for oobm? dynamic-checker has no update counter in any of the api or schema. If this was for oobm, the update counter is used only for updating the power state and there is no API exposing or requiring this field. The power action 'status' causes update of the power state if it's different (or if ownership has changed only).


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60771374
  
    --- Diff: plugins/user-authenticators/ldap/src/org/apache/cloudstack/api/command/LdapCreateAccountCmd.java ---
    @@ -119,6 +131,9 @@ private Long getDomainId() {
     
         @Override
         public void execute() throws ServerApiException {
    +        if (getAccountType() == null && getRoleId() == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Both account type and role ID are not provided");
    --- End diff --
    
    @jburwell because of backward compatibility we cannot make roleid param as required, while accounttype used to be required but is now made non-required; this checks ensures that at least one of them is passed. When only account type is passed, we translate that to one of the four default roles (id).


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215479369
  
    In reading this, is it the case that if someone upgrades from a previous version to 4.9 (with this code included) they will not be able to take advantage of this functionality?  Maybe I did not understand those details correctly.  This is just a clarification so I know what to expect (I have customers who are interested in this functionality).
    
    @koushik-das Regarding: "Regarding writing a plugin, I meant that if someone really needed a functionality like this, they could have easily developed it outside of Cloudstack as well (like some kind of portal calling Cloudstack APIs)."  Given that I work a company who has done exactly that, I can assure you it is not "easy" and takes a huge development effort to recreate all the functionality provided by the ACS UI.  This is not really a viable option for people looking for this feature.
    
    @koushik-das I also don't understand this "Any blocker (including security bugs) in this will make the 4.9 release unusable for new installs as this the only authorisation mechanism for entering into the system.".  Can you explain what you mean?  How does this feature and a 'blocker bug' render 4.9 unusable?  I am not quite following this.  Maybe someone else can also chime in to help explain his concerns.
    
    Thx...


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59787000
  
    --- Diff: api/src/org/apache/cloudstack/acl/Rule.java ---
    @@ -0,0 +1,42 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.google.common.base.Strings;
    +
    +public final class Rule {
    +    private final String rule;
    +    private final static String ALLOWED_PATTERN = "^[a-zA-Z0-9*]+$";
    --- End diff --
    
    Aren't wildcard characters were only allowed as the first or last character in the rule specification?  If so, then this regex needs to be tightened up.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-212065155
  
    @swill just completed local build here: (also pushed -f, let's see if Travis reports any failure)
    
    [INFO] ------------------------------------------------------------------------
    [INFO] Building Apache CloudStack apidocs 4.9.0-SNAPSHOT
    [INFO] ------------------------------------------------------------------------
    [INFO] 
    [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ cloud-apidoc ---
    [INFO] Deleting /home/bhaisaab/Lab/apache/cloudstack/tools/apidoc/target (includes = [**/*], excludes = [])
    [INFO] Deleting /home/bhaisaab/Lab/apache/cloudstack/tools/apidoc (includes = [target, dist], excludes = [])
    [INFO] 
    [INFO] --- maven-checkstyle-plugin:2.11:check (cloudstack-checkstyle) @ cloud-apidoc ---
    [INFO] Starting audit...
    Audit done.
    
    [INFO] 
    [INFO] --- maven-remote-resources-plugin:1.3:process (default) @ cloud-apidoc ---
    [INFO] 
    [INFO] --- exec-maven-plugin:1.2.1:exec (compile) @ cloud-apidoc ---
    log4j:WARN No appenders could be found for logger (org.reflections.Reflections).
    log4j:WARN Please initialize the log4j system properly.
    log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
    Warning, API Cmd class com.cloud.api.commands.SimulatorAddSecondaryAgent has no APICommand annotation 
    Scanned and found 557 APIs
    [INFO] 
    [INFO] --- maven-site-plugin:3.1:attach-descriptor (attach-descriptor) @ cloud-apidoc ---
    [INFO] 
    [INFO] --- exec-maven-plugin:1.2.1:exec (package) @ cloud-apidoc ---
    [INFO] 
    [INFO] --- maven-install-plugin:2.3.1:install (default-install) @ cloud-apidoc ---
    [INFO] Installing /home/bhaisaab/Lab/apache/cloudstack/tools/apidoc/pom.xml to /home/bhaisaab/.m2/repository/org/apache/cloudstack/cloud-apidoc/4.9.0-SNAPSHOT/cloud-apidoc-4.9.0-SNAPSHOT.pom


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59513920
  
    --- Diff: api/test/org/apache/cloudstack/acl/RoleTypeTest.java ---
    @@ -0,0 +1,92 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.user.Account;
    +import org.junit.Assert;
    +import org.junit.Test;
    +import org.mockito.Mockito;
    +
    +import java.util.Arrays;
    +
    +public class RoleTypeTest {
    +
    +    @Test
    +    public void testRoleTypeFromString() {
    +        Assert.assertEquals(RoleType.fromString(null), null);
    +        Assert.assertEquals(RoleType.fromString(""), null);
    +        Assert.assertEquals(RoleType.fromString("admin"), null);
    +        Assert.assertEquals(RoleType.fromString("12345%^&*"), null);
    +        for (RoleType roleType : RoleType.values()) {
    +            Assert.assertEquals(RoleType.fromString(roleType.name()), roleType);
    --- End diff --
    
    Unknown should be allowed to be created as "Unknown"? would seem we want to guard against that.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-210423416
  
    Thanks all, I've fixed most issues. Please see any outstanding issue and let me know if you find any other issue.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-217264750
  
    @rhtyd we have merge conflicts.  can you fix?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216851115
  
    We performed end-to-end testing of this PR by building packages today [1] and found an issue with maven (not related to this PR) where users get sent to error.jsp due to a missing jstl.jar. The 4th commit on this PR fixed that issue.
    
    [1] https://packages.shapeblue.com/cloudstack/custom/dynamicroles/


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by borisroman <gi...@git.apache.org>.
Github user borisroman commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216487485
  
    @swill I think you mentioned the wrong Boris :)



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-209026886
  
    @DaanHoogland I was pushing some changes and adding some info/screenshots above. Since this aims to deprecate use of commands.properties (affecting marvin and apidocs) I've a separate commit for that in this PR.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59800993
  
    --- Diff: api/test/org/apache/cloudstack/acl/RuleTest.java ---
    @@ -0,0 +1,39 @@
    +package org.apache.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import org.junit.Assert;
    +import org.junit.Test;
    +
    +import java.util.Arrays;
    +
    +public class RuleTest {
    +
    +    @Test
    +    public void testToString() throws Exception {
    +        Rule rule = new Rule("someString");
    +        Assert.assertEquals(rule.toString(), "someString");
    +    }
    +
    +    @Test
    +    public void testValidateRuleWithValidData() throws Exception {
    +        for (String rule : Arrays.asList("a", "1", "someApi", "someApi321", "123SomeApi",
    +                "prefix*", "*middle*", "*Suffix",
    +                "*", "**", "f***", "m0nk3yMa**g1c*")) {
    +            Assert.assertTrue(Rule.validate(rule));
    +            Assert.assertEquals(new Rule(rule).toString(), rule);
    +        }
    +    }
    +
    +    @Test
    +    public void testValidateRuleWithInvalidData() throws Exception {
    +        for (String rule : Arrays.asList(null, "", " ", "  ", "\n", "\t", "\r", "\"", "\'",
    +                "^someApi$", "^someApi", "some$", "some-Api;", "some,Api",
    +                "^", "$", "^$", ".*", "\\w+", "r**l3rd0@Kr3", "j@s1n|+|0ȷ",
    +                "[a-z0-9-]+", "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$")) {
    +            try {
    +                new Rule(rule);
    +                Assert.fail("Invalid rule, exception was expected");
    +            } catch (InvalidParameterValueException ignored) {}
    +        }
    --- End diff --
    
    This method can be simplified by removing the try-catch and added ``expected=InvalidParameterValueException.class`` to the ``@Test`` annotation.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60880160
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    +        return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10))
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_list(self):
    +        """
    +            Tests that default four roles exist
    +        """
    +        roleTypes = {1: "Admin", 2: "ResourceAdmin", 3: "DomainAdmin", 4: "User"}
    +        for idx in range(1,5):
    +            list_roles = Role.list(self.apiclient, id=idx)
    +            self.assertEqual(
    +                isinstance(list_roles, list),
    +                True,
    +                "List Roles response was not a valid list"
    +            )
    +            self.assertEqual(
    +                len(list_roles),
    +                1,
    +                "List Roles response size was not 1"
    +            )
    +            self.assertEqual(
    +                list_roles[0].type,
    +                roleTypes[idx],
    +                msg="Default role type differs from expectation"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_create(self):
    +        """
    +            Tests normal lifecycle operations for roles
    +        """
    +        # Reuse self.role created in setUp()
    +        try:
    +            role = Role.create(
    +                self.apiclient,
    +                self.testdata["role"]
    +            )
    +            self.fail("An exception was expected when creating duplicate roles")
    +        except CloudstackAPIException: pass
    +
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_roles, list),
    +            True,
    +            "List Roles response was not a valid list"
    +        )
    +        self.assertEqual(
    +            len(list_roles),
    +            1,
    +            "List Roles response size was not 1"
    +        )
    +        self.assertEqual(
    +            list_roles[0].name,
    +            self.testdata["role"]["name"],
    +            msg="Role name does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_roles[0].type,
    +            self.testdata["role"]["type"],
    +            msg="Role type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +        self.assertEqual(
    +            update_role.type,
    +            'Admin',
    +            msg="Role type does not match updated role type"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update_role_inuse(self):
    +        """
    +            Tests role update when role is in use by an account
    +        """
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        try:
    +            self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +            self.fail("Updation of role type is not allowed when role is in use")
    +        except CloudstackAPIException: pass
    +
    +        self.role.update(self.apiclient, name=new_role_name)
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_delete(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        self.role.delete(self.apiclient)
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            list_roles,
    +            None,
    +            "List Roles response should be empty"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_inuse_deletion(self):
    +        """
    +            Test to ensure role in use cannot be deleted
    +        """
    +        try:
    +            self.role.delete(self.apiclient)
    +            self.fail("Role with any account should not be allowed to be deleted")
    +        except CloudstackAPIException: pass
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_default_role_deletion(self):
    +        """
    +            Test to ensure 4 default roles cannot be deleted
    +        """
    +        for idx in range(1,5):
    +            cmd = deleteRole.deleteRoleCmd()
    +            cmd.id = idx
    +            try:
    +                self.apiclient.deleteRole(cmd)
    +                self.fail("Default role got deleted with id: " + idx)
    +            except CloudstackAPIException: pass
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_list(self):
    +        """
    +            Tests listing of default role's permission
    +        """
    +        for idx in range(1,5):
    +            list_rolepermissions = RolePermission.list(self.apiclient, roleid=idx)
    +            self.assertEqual(
    +                isinstance(list_rolepermissions, list),
    +                True,
    +                "List rolepermissions response was not a valid list"
    +            )
    +            self.assertTrue(
    +                len(list_rolepermissions) > 0,
    +                "List rolepermissions response was empty"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_create(self):
    +        """
    +            Tests creation of role permission
    +        """
    +        # Reuse self.rolepermission created in setUp()
    +        try:
    +            rolepermission = RolePermission.create(
    +                self.apiclient,
    +                self.testdata["rolepermission"]
    +            )
    +            self.fail("An exception was expected when creating duplicate role permissions")
    +        except CloudstackAPIException: pass
    +
    +        list_rolepermissions = RolePermission.list(self.apiclient, roleid=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_rolepermissions, list),
    +            True,
    +            "List rolepermissions response was not a valid list"
    +        )
    +        self.assertNotEqual(
    +            len(list_rolepermissions),
    +            0,
    +            "List rolepermissions response was empty"
    +        )
    +        self.assertEqual(
    +            list_rolepermissions[0].rule,
    +            self.testdata["rolepermission"]["rule"],
    +            msg="Role permission rule does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_rolepermissions[0].permission,
    +            self.testdata["rolepermission"]["permission"],
    +            msg="Role permission permission-type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_update(self):
    +        """
    +            Tests updation of role permission
    +        """
    +        new_rule = "some*Rule" + self.getRandomString()
    +        self.rolepermission.update(self.apiclient, rule=new_rule, permission='deny')
    +        updated_rolepermission = filter(lambda p: p.id == self.rolepermission.id,
    +                                        RolePermission.list(self.apiclient, roleid=self.role.id))[0]
    +        self.assertEqual(
    +            updated_rolepermission.rule,
    +            new_rule,
    +            msg="Role permission rule does not match updated role permission"
    +        )
    +        self.assertEqual(
    +            updated_rolepermission.permission,
    +            'deny',
    +            msg="Role permission permission-type does not match updated role permission"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_update_invalid_rule(self):
    +        """
    +            Tests updation of role permission with an invalid rule
    +        """
    +        for badrule in [" ", "^delete.*$", "some-@p1-!"]:
    +            try:
    +                self.rolepermission.update(self.apiclient, rule=badrule, permission='deny')
    +                self.fail("Invalid role permission rule got updated")
    +            except CloudstackAPIException: pass
    --- End diff --
    
    We can do that but since we've catching any ACS server api exception caught by CloudStackAPIException, we know it's ACS specific API exception and not any HTTP exception.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215501683
  
    @swill existing users won't be automatically migrated to use this feature, see the admin-doc PR that documents the upgrade/migrate procedure; though new installations will use dynamic-checker by default.
    
    Will, while the PR is ready to be merged -- I think I can improve the UI/UX around how rules are added and evaluated (for example, we may drag/drop rules, make it more intuitive etc) and I would also like to improve the docs on using the dynamic-checker wrt rules (API name and wildcard rules)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59513255
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/ListRolesCmd.java ---
    @@ -0,0 +1,118 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.exception.ConcurrentOperationException;
    +import com.cloud.exception.InsufficientCapacityException;
    +import com.cloud.exception.NetworkRuleConflictException;
    +import com.cloud.exception.ResourceAllocationException;
    +import com.cloud.exception.ResourceUnavailableException;
    +import com.cloud.user.Account;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.ListResponse;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +@APICommand(name = ListRolesCmd.APINAME, description = "Lists dynamic roles in CloudStack", responseObject = RoleResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin})
    +public class ListRolesCmd extends BaseCmd {
    +    public static final String APINAME = "listRoles";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = RoleResponse.class, description = "List role by role ID.")
    +    private Long id;
    +
    +    @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "List role by role name.")
    +    private String roleName;
    +
    +    @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "List role by role type, valid options are: Admin, ResourceAdmin, DomainAdmin, User.")
    +    private String roleType;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public Long getId() {
    +        return id;
    +    }
    +
    +    public String getName() {
    +        return roleName;
    +    }
    +
    +    public RoleType getRoleType() {
    +        if (!Strings.isNullOrEmpty(roleType)) {
    +            return RoleType.valueOf(roleType);
    +        }
    +        return null;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
    +        if (getId() != null && getId() < 1L) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    +        List<Role> roles = roleService.findAllRolesBy(getId(), getName(), getRoleType());
    +        ListResponse<RoleResponse> response = new ListResponse<>();
    +        List<RoleResponse> roleResponses = new ArrayList<>();
    +        for (Role role : roles) {
    +            if (role == null) {
    +                continue;
    +            }
    +            RoleResponse roleResponse = new RoleResponse();
    +            roleResponse.setId(role.getUuid());
    +            roleResponse.setRoleName(role.getName());
    +            roleResponse.setRoleType(role.getRoleType());
    +            roleResponse.setDescription(role.getDescription());
    +            roleResponse.setObjectName("role");
    +            roleResponses.add(roleResponse);
    +        }
    +        response.setResponses(roleResponses);
    +        response.setResponseName(getCommandName());
    +        setResponseObject(response);
    +    }
    --- End diff --
    
    same as create commands: factor out?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59801445
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    +
    +                } catch (SQLException e) {
    +                    s_logger.error("Failed to update cloud.account role_id for account id:" + accountId + " with exception: " + e.getMessage());
    +                    throw new CloudRuntimeException("Exception while updating cloud.account role_id", e);
    +                }
    +            }
    --- End diff --
    
    Should these updates be wrapped in a transaction to prevent data inconsistencies if there is an exception?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by pdube <gi...@git.apache.org>.
Github user pdube commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59730081
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleType.java ---
    @@ -16,18 +16,90 @@
     // under the License.
     package org.apache.cloudstack.acl;
     
    +import com.cloud.user.Account;
    +import com.google.common.base.Enums;
    +import com.google.common.base.Strings;
    +
     // Enum for default roles in CloudStack
     public enum RoleType {
    -    Admin(1), ResourceAdmin(2), DomainAdmin(4), User(8), Unknown(0);
    +    Admin(1L, Account.ACCOUNT_TYPE_ADMIN, 1),
    +    ResourceAdmin(2L, Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN, 2),
    +    DomainAdmin(3L, Account.ACCOUNT_TYPE_DOMAIN_ADMIN, 4),
    +    User(4L, Account.ACCOUNT_TYPE_NORMAL, 8),
    +    Unknown(-1L, (short) -1, 0);
     
    +    private long id;
    +    private short accountType;
         private int mask;
     
    -    private RoleType(int mask) {
    +    RoleType(final long id, final short accountType, final int mask) {
    +        this.id = id;
    +        this.accountType = accountType;
             this.mask = mask;
         }
     
    -    public int getValue() {
    +    public long getId() {
    +        return id;
    +    }
    +
    +    public short getAccountType() {
    +        return accountType;
    +    }
    +
    +    public int getMask() {
             return mask;
         }
    -}
     
    +    public static RoleType fromString(final String name) {
    +        if (!Strings.isNullOrEmpty(name)
    +                && Enums.getIfPresent(RoleType.class, name).isPresent()) {
    +            return RoleType.valueOf(name);
    +        }
    +        return null;
    +    }
    +
    +    public static RoleType fromMask(int mask) {
    +        for (RoleType roleType : RoleType.values()) {
    +            if (roleType.getMask() == mask) {
    +                return roleType;
    +            }
    +        }
    +        return Unknown;
    +    }
    +
    +    public static RoleType getByAccountType(final short accountType) {
    +        RoleType roleType = RoleType.Unknown;
    +        switch (accountType) {
    +            case Account.ACCOUNT_TYPE_ADMIN:
    +                roleType = RoleType.Admin;
    +                break;
    +            case Account.ACCOUNT_TYPE_DOMAIN_ADMIN:
    +                roleType = RoleType.DomainAdmin;
    +                break;
    +            case Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN:
    +                roleType = RoleType.ResourceAdmin;
    --- End diff --
    
    What is this role type? Is there any documentation on it?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60744764
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    +        return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10))
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_list(self):
    +        """
    +            Tests that default four roles exist
    +        """
    +        roleTypes = {1: "Admin", 2: "ResourceAdmin", 3: "DomainAdmin", 4: "User"}
    +        for idx in range(1,5):
    +            list_roles = Role.list(self.apiclient, id=idx)
    +            self.assertEqual(
    +                isinstance(list_roles, list),
    +                True,
    +                "List Roles response was not a valid list"
    +            )
    +            self.assertEqual(
    +                len(list_roles),
    +                1,
    +                "List Roles response size was not 1"
    +            )
    +            self.assertEqual(
    +                list_roles[0].type,
    +                roleTypes[idx],
    +                msg="Default role type differs from expectation"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_create(self):
    +        """
    +            Tests normal lifecycle operations for roles
    +        """
    +        # Reuse self.role created in setUp()
    +        try:
    +            role = Role.create(
    +                self.apiclient,
    +                self.testdata["role"]
    +            )
    +            self.fail("An exception was expected when creating duplicate roles")
    +        except CloudstackAPIException: pass
    +
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_roles, list),
    +            True,
    +            "List Roles response was not a valid list"
    +        )
    +        self.assertEqual(
    +            len(list_roles),
    +            1,
    +            "List Roles response size was not 1"
    +        )
    +        self.assertEqual(
    +            list_roles[0].name,
    +            self.testdata["role"]["name"],
    +            msg="Role name does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_roles[0].type,
    +            self.testdata["role"]["type"],
    +            msg="Role type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +        self.assertEqual(
    +            update_role.type,
    +            'Admin',
    +            msg="Role type does not match updated role type"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update_role_inuse(self):
    +        """
    +            Tests role update when role is in use by an account
    +        """
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        try:
    +            self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +            self.fail("Updation of role type is not allowed when role is in use")
    +        except CloudstackAPIException: pass
    +
    +        self.role.update(self.apiclient, name=new_role_name)
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_delete(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        self.role.delete(self.apiclient)
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            list_roles,
    +            None,
    +            "List Roles response should be empty"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_inuse_deletion(self):
    +        """
    +            Test to ensure role in use cannot be deleted
    +        """
    +        try:
    +            self.role.delete(self.apiclient)
    +            self.fail("Role with any account should not be allowed to be deleted")
    +        except CloudstackAPIException: pass
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_default_role_deletion(self):
    +        """
    +            Test to ensure 4 default roles cannot be deleted
    +        """
    +        for idx in range(1,5):
    +            cmd = deleteRole.deleteRoleCmd()
    +            cmd.id = idx
    +            try:
    +                self.apiclient.deleteRole(cmd)
    +                self.fail("Default role got deleted with id: " + idx)
    +            except CloudstackAPIException: pass
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_list(self):
    +        """
    +            Tests listing of default role's permission
    +        """
    +        for idx in range(1,5):
    +            list_rolepermissions = RolePermission.list(self.apiclient, roleid=idx)
    +            self.assertEqual(
    +                isinstance(list_rolepermissions, list),
    +                True,
    +                "List rolepermissions response was not a valid list"
    +            )
    +            self.assertTrue(
    +                len(list_rolepermissions) > 0,
    +                "List rolepermissions response was empty"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_create(self):
    +        """
    +            Tests creation of role permission
    +        """
    +        # Reuse self.rolepermission created in setUp()
    +        try:
    +            rolepermission = RolePermission.create(
    +                self.apiclient,
    +                self.testdata["rolepermission"]
    +            )
    +            self.fail("An exception was expected when creating duplicate role permissions")
    +        except CloudstackAPIException: pass
    --- End diff --
    
    Is there any value asserting on the contents of the error message?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60357104
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            final String rule = permission.getRule();
    +            if (rule.contains("*")) {
    +                if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                    return true;
    +                }
    +            } else {
    +                if (commandName.equals(rule)) {
    --- End diff --
    
    Should the equality check ignore case?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59856830
  
    --- Diff: api/test/org/apache/cloudstack/acl/RoleTypeTest.java ---
    @@ -0,0 +1,92 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.user.Account;
    +import org.junit.Assert;
    +import org.junit.Test;
    +import org.mockito.Mockito;
    +
    +import java.util.Arrays;
    +
    +public class RoleTypeTest {
    +
    +    @Test
    +    public void testRoleTypeFromString() {
    +        Assert.assertEquals(RoleType.fromString(null), null);
    +        Assert.assertEquals(RoleType.fromString(""), null);
    +        Assert.assertEquals(RoleType.fromString("admin"), null);
    +        Assert.assertEquals(RoleType.fromString("12345%^&*"), null);
    --- End diff --
    
    This method is used by create/update role apicmds, fixed it to throw exception.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-209626989
  
    
    ### CI bubble run
    
    
    
    
    initially test_02_redundant_VPC_default_routes failed. On manual verification it succeeded.
    
    ```
    [root@cs1 MarvinLogs]# ssh 192.168.23.5
    The authenticity of host '192.168.23.5 (192.168.23.5)' can't be established.
    ECDSA key fingerprint is 61:af:15:ea:84:48:c4:78:3c:e0:b3:d5:44:30:fe:dc.
    Are you sure you want to continue connecting (yes/no)? yes
    Warning: Permanently added '192.168.23.5' (ECDSA) to the list of known hosts.
    root@192.168.23.5's password: 
    # ping -c 3 8.8.8.8
    PING 8.8.8.8 (8.8.8.8): 56 data bytes
    64 bytes from 8.8.8.8: seq=0 ttl=46 time=23.640 ms
    64 bytes from 8.8.8.8: seq=1 ttl=46 time=32.166 ms
    64 bytes from 8.8.8.8: seq=2 ttl=46 time=23.668 ms
    
    --- 8.8.8.8 ping statistics ---
    3 packets transmitted, 3 packets received, 0% packet loss
    round-trip min/avg/max = 23.640/26.491/32.166 ms
    ```
    
    
    



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60802098
  
    --- Diff: utils/src/main/java/com/cloud/utils/PropertiesUtil.java ---
    @@ -34,6 +34,10 @@
     public class PropertiesUtil {
         private static final Logger s_logger = Logger.getLogger(PropertiesUtil.class);
     
    +    public static String getDefaultApiCommandsFileName() {
    +        return "commands.properties";
    +    }
    --- End diff --
    
    @bhaisaab my thinking is that commands.properties is only managed/used by the Authenticator mechanism.  Is it used more widely?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60944411
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,478 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    --- End diff --
    
    @koushik-das (3) see ^^ this method can get us roletype or user specific api client


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59510155
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleService.java ---
    @@ -0,0 +1,43 @@
    +// 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.cloudstack.acl;
    +
    +import org.apache.cloudstack.framework.config.ConfigKey;
    +
    +import java.util.List;
    +
    +public interface RoleService {
    +
    +    ConfigKey<Boolean> EnableDynamicApiChecker = new ConfigKey<>("Hidden", Boolean.class, "dynamic.apichecker.enabled", "false",
    +            "If set to true, this enables the dynamic role-based api access checker and disables the default static role-based api access checker.",
    +            true);
    +
    +    boolean isEnabled();
    +    Role findRole(final Long id);
    +    Role createRole(final String name, final RoleType roleType, final String description);
    +    boolean updateRole(final Role role, final String name, final RoleType roleType, final String description);
    +    boolean deleteRole(final Role role);
    +
    +    RolePermission findRolePermission(final Long id);
    +    RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description);
    +    boolean updateRolePermission(final RolePermission rolePermission, final Rule rule, final RolePermission.Permission permission, final String description);
    +    boolean deleteRolePermission(final RolePermission rolePermission);
    +
    +    List<Role> findAllRolesBy(final Long id, final String name, final RoleType roleType);
    --- End diff --
    
    findAllRolesBy all three parameters? or either one of them? I will probably find out reading on but a comment/javadoc in this case would be nice (you know I'm not a fan of comments, right ;)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59855818
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/CreateRoleCmd.java ---
    @@ -0,0 +1,104 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.user.Account;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +import org.apache.cloudstack.context.CallContext;
    +
    +@APICommand(name = CreateRoleCmd.APINAME, description = "Creates a role", responseObject = RoleResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class CreateRoleCmd extends BaseCmd {
    +    public static final String APINAME = "createRole";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "creates a role with this unique name")
    +    private String roleName;
    +
    +    @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "The type of the role, valid options are: Admin, ResourceAdmin, DomainAdmin, User")
    +    private String roleType;
    +
    +    @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the role")
    +    private String roleDescription;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public String getRoleName() {
    +        return roleName;
    +    }
    +
    +    public RoleType getRoleType() {
    +        return RoleType.fromString(roleType);
    +    }
    +
    +    public String getRoleDescription() {
    +        return roleDescription;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() {
    +        if (Strings.isNullOrEmpty(getRoleName())) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty role name provided");
    +        }
    +        if (getRoleType() == null || getRoleType() == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
    +        }
    --- End diff --
    
    fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by borisstoyanov <gi...@git.apache.org>.
Github user borisstoyanov commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216550819
  
    Thanks @swill as Rohit mentioned we did migration testing of 4.8.1 to 4.9 without enabling the feature, to confirm users are able to continue using CS without dynamic-checker. Then we executed the migration script and verified the commands.properties are no longer available and all the rules are migrated in the DB. This was performed with regular users as well as LDAP and SAML users. Fresh installation of 4.9 was also tested, by default dynamic-checker is enabled. Also regression testing was performed on creating and using custom rules. My LGTM is up in the thread. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-217500553
  
    Thanks @swill as soon as we can merge this one, I can rebase and use some of the annotations work and ListUtils from this PR into out-of-band management PR /cc @jburwell 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-217606327
  
    
    
    ### CI RESULTS
    
    ```
    Tests Run: 85
      Skipped: 0
       Failed: 2
       Errors: 0
     Duration: 6h 11m 18s
    ```
    
    **Summary of the problem(s):**
    ```
    FAIL: Test redundant router internals
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/data/git/cs2/cloudstack/test/integration/smoke/test_routers_network_ops.py", line 290, in test_01_RVR_Network_FW_PF_SSH_default_routes_egress_true
        "Attempt to retrieve google.com index page should be successful!"
    AssertionError: Attempt to retrieve google.com index page should be successful!
    ----------------------------------------------------------------------
    Additional details in: /tmp/MarvinLogs/test_network_MN3MB5/results.txt
    ```
    
    ```
    FAIL: Test redundant router internals
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/data/git/cs2/cloudstack/test/integration/smoke/test_routers_network_ops.py", line 483, in test_02_RVR_Network_FW_PF_SSH_default_routes_egress_false
        "Attempt to retrieve google.com index page should be successful once rule is added!"
    AssertionError: Attempt to retrieve google.com index page should be successful once rule is added!
    ----------------------------------------------------------------------
    Additional details in: /tmp/MarvinLogs/test_network_MN3MB5/results.txt
    ```
    
    
    
    **Associated Uploads**
    
    **`/tmp/MarvinLogs/DeployDataCenter__May_06_2016_21_34_18_ZVNY8R:`**
    * [dc_entries.obj](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/DeployDataCenter__May_06_2016_21_34_18_ZVNY8R/dc_entries.obj)
    * [failed_plus_exceptions.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/DeployDataCenter__May_06_2016_21_34_18_ZVNY8R/failed_plus_exceptions.txt)
    * [runinfo.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/DeployDataCenter__May_06_2016_21_34_18_ZVNY8R/runinfo.txt)
    
    **`/tmp/MarvinLogs/test_network_MN3MB5:`**
    * [failed_plus_exceptions.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_network_MN3MB5/failed_plus_exceptions.txt)
    * [results.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_network_MN3MB5/results.txt)
    * [runinfo.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_network_MN3MB5/runinfo.txt)
    
    **`/tmp/MarvinLogs/test_vpc_routers_DZWFXD:`**
    * [failed_plus_exceptions.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_vpc_routers_DZWFXD/failed_plus_exceptions.txt)
    * [results.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_vpc_routers_DZWFXD/results.txt)
    * [runinfo.txt](https://objects-east.cloud.ca/v1/e465abe2f9ae4478b9fff416eab61bd9/PR1489/tmp/MarvinLogs/test_vpc_routers_DZWFXD/runinfo.txt)
    
    
    Uploads will be available until `2016-07-07 02:00:00 +0200 CEST`
    
    *Comment created by [`upr comment`](https://github.com/cloudops/upr).*



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-219986204
  
    @anshul1886 I'm unable to reproduce the shared issues nor they were caught by CI/Jenkins/Travis runs. The role specific rules in role_permissions table have been copied from the last commands.properties.in file. If you've added new APIs they need to use authorized annotations.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-219988321
  
    @anshul1886 with dynamic-roles feature, we no longer want to use or promote use of commands.properties file which is why this has been removed in master. We no longer want to add changes/new APIs to this file as well though existing installations will continue to use the static-checker until they decide to upgrade.
    
    You need to use the annotations as described on the wiki (this has been shared recently on dev ML as well):
    https://cwiki.apache.org/confluence/display/CLOUDSTACK/Annotations+use+in+the+API
    https://cwiki.apache.org/confluence/display/CLOUDSTACK/CloudStack+API+Development
    
    I think you've not rebuilt your branch after a rebase, you need to perform a clean rebuild.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by abhinandanprateek <gi...@git.apache.org>.
Github user abhinandanprateek commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220239213
  
    @rhtyd @anshul1886 This week I rebased the ospf changes with current master and did not have any issues. OSPF adds 2 new APIs and they are using the new scheme of things, looked good to Marvin and UI.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-218359137
  
    Thank you.  I am really trying to avoid that, but lets see what happens.  :)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216486309
  
    @rhtyd for optimistic locking, is the update counter returned on the get APIs and required on the update APIs?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by borisstoyanov <gi...@git.apache.org>.
Github user borisstoyanov commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-212127186
  
    @bhaisaab @swill Hey Guys, I've successfully checkout and build the PR, will continue with testing tomorrow. I'd like to take the opportunity to tell you I've executed marvin and end to end regression tests on our private branch and it passed. So regarding testing this code base I'm not really expecting issues to pop up. Will execute the tests tomorrow and will keep you posted. Have a good night. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60732582
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    +
    +                } catch (SQLException e) {
    +                    s_logger.error("Failed to update cloud.account role_id for account id:" + accountId + " with exception: " + e.getMessage());
    +                    throw new CloudRuntimeException("Exception while updating cloud.account role_id", e);
    +                }
    +            }
    --- End diff --
    
    so the upgrade process can leave the database in an inconsistent state?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60744528
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    +        return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10))
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_list(self):
    +        """
    +            Tests that default four roles exist
    +        """
    +        roleTypes = {1: "Admin", 2: "ResourceAdmin", 3: "DomainAdmin", 4: "User"}
    +        for idx in range(1,5):
    +            list_roles = Role.list(self.apiclient, id=idx)
    +            self.assertEqual(
    +                isinstance(list_roles, list),
    +                True,
    +                "List Roles response was not a valid list"
    +            )
    +            self.assertEqual(
    +                len(list_roles),
    +                1,
    +                "List Roles response size was not 1"
    +            )
    +            self.assertEqual(
    +                list_roles[0].type,
    +                roleTypes[idx],
    +                msg="Default role type differs from expectation"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_create(self):
    +        """
    +            Tests normal lifecycle operations for roles
    +        """
    +        # Reuse self.role created in setUp()
    +        try:
    +            role = Role.create(
    +                self.apiclient,
    +                self.testdata["role"]
    +            )
    +            self.fail("An exception was expected when creating duplicate roles")
    +        except CloudstackAPIException: pass
    +
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_roles, list),
    +            True,
    +            "List Roles response was not a valid list"
    +        )
    +        self.assertEqual(
    +            len(list_roles),
    +            1,
    +            "List Roles response size was not 1"
    +        )
    +        self.assertEqual(
    +            list_roles[0].name,
    +            self.testdata["role"]["name"],
    +            msg="Role name does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_roles[0].type,
    +            self.testdata["role"]["type"],
    +            msg="Role type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +        self.assertEqual(
    +            update_role.type,
    +            'Admin',
    +            msg="Role type does not match updated role type"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update_role_inuse(self):
    +        """
    +            Tests role update when role is in use by an account
    +        """
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    --- End diff --
    
    Minor Nit: This line appears throughout the test case.  Have you considered a ``randomRolename`` function?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215008749
  
    >>That is to say that the API permissions on a fresh install before and after this is merged should behave the same out of the box. If that's the case then I don't think users will feel forced into anything or even notice that something was changed, and if someone really does care, it sounds simple enough to enable commands.properties.
    
    @mlsorensen @jburwell Unless users try it out how will they verify that the behaviour is same out of box before and after. I know testing have been done but is it good enough to say that things are consistent before and after. Note that there is data migration happening from commands.properties to DB. I know that commands.properties has it limitations but that doesn't mean it needs to be removed immediately. Let the new feature be there for atleast a couple of releases so that it can be tried out and   stabilized before deprecating the old one. If the concern is about the file getting changed then that can be easily prevented (same is done for the old db schema files as well). All I am saying is if the file needs to be removed do it after a few releases by following the proper deprecation process.
    
    >>Second, command.properties does not permit the definition of new roles. Limiting users to 4 roles in a modern cloud environment is a barrier to CloudStack adoption.
    
    @jburwell Correct me if I am wrong but based on what I have seen in the code even after this feature there will still be 4 roles. I think what this feature allows is creating some grouping of permissions and assigning them a name. I can create a role with name "Operator" having say API1 and API2, someone else can create a role with same name but with API3 and API4. The same can be implemented as an independent plugin outside of cloudstack as well. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-212065979
  
    @borisstoyanov would like to share our QA results, thanks.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60384825
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            final String rule = permission.getRule();
    +            if (rule.contains("*")) {
    +                if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                    return true;
    +                }
    +            } else {
    +                if (commandName.equals(rule)) {
    +                    return true;
    +                }
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    --- End diff --
    
    If default is deny then there is no need for 'DENY' based permissions. In that case you don't need to store allow/deny in DB as all permissions will be allow only. Let me know if this is not correct.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60744168
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    --- End diff --
    
    Would this function be valuable to other tests?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60801905
  
    --- Diff: plugins/user-authenticators/ldap/src/org/apache/cloudstack/api/command/LdapCreateAccountCmd.java ---
    @@ -119,6 +131,9 @@ private Long getDomainId() {
     
         @Override
         public void execute() throws ServerApiException {
    +        if (getAccountType() == null && getRoleId() == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Both account type and role ID are not provided");
    --- End diff --
    
    @bhaisaab this check seems more restrictive than the previous version.  Before this change, ``accountType`` was completely optional.  With this change, an script using this API call that did not include ``accountType`` and wouldn't include ``roleId``because it is a new parameter would fail.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60881906
  
    --- Diff: plugins/user-authenticators/ldap/src/org/apache/cloudstack/api/command/LdapImportUsersCmd.java ---
    @@ -70,10 +72,12 @@
     
         @Parameter(name = ApiConstants.ACCOUNT_TYPE,
                    type = CommandType.SHORT,
    -               required = true,
    --- End diff --
    
    @jburwell see here ^^, accountype was a required arg but with our changes we've made it non-required also added the roleId arg. Therefore, at least one of the two must be provided, if both are provided we consider roleId (and account type of the role).


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59522641
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleService.java ---
    @@ -0,0 +1,43 @@
    +// 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.cloudstack.acl;
    +
    +import org.apache.cloudstack.framework.config.ConfigKey;
    +
    +import java.util.List;
    +
    +public interface RoleService {
    +
    +    ConfigKey<Boolean> EnableDynamicApiChecker = new ConfigKey<>("Hidden", Boolean.class, "dynamic.apichecker.enabled", "false",
    +            "If set to true, this enables the dynamic role-based api access checker and disables the default static role-based api access checker.",
    +            true);
    +
    +    boolean isEnabled();
    +    Role findRole(final Long id);
    +    Role createRole(final String name, final RoleType roleType, final String description);
    +    boolean updateRole(final Role role, final String name, final RoleType roleType, final String description);
    +    boolean deleteRole(final Role role);
    +
    +    RolePermission findRolePermission(final Long id);
    +    RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description);
    +    boolean updateRolePermission(final RolePermission rolePermission, final Rule rule, final RolePermission.Permission permission, final String description);
    +    boolean deleteRolePermission(final RolePermission rolePermission);
    +
    +    List<Role> findAllRolesBy(final Long id, final String name, final RoleType roleType);
    --- End diff --
    
    This method tries to solve data processing for listRoles API, it can list by id (i.e. if exists, single item returned) , or returns list of role matching a name, or if nothing else passed checks if a role type was passed. @DaanHoogland  do you want me to split them up into separate methods? 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60772858
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,138 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.warn("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    --- End diff --
    
    Thanks, will fix this.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59855822
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/DeleteRoleCmd.java ---
    @@ -0,0 +1,84 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.user.Account;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +import org.apache.cloudstack.api.response.SuccessResponse;
    +import org.apache.cloudstack.context.CallContext;
    +
    +@APICommand(name = DeleteRoleCmd.APINAME, description = "Deletes a role", responseObject = SuccessResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class DeleteRoleCmd extends BaseCmd {
    +    public static final String APINAME = "deleteRole";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.ID, type = BaseCmd.CommandType.UUID, required = true, entityType = RoleResponse.class, description = "ID of the role")
    +    private Long roleId;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public Long getRoleId() {
    +        return roleId;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() {
    +        if (getRoleId() == null || getRoleId() < 1L) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    +        Role role = roleService.findRole(getRoleId());
    +        if (role == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    --- End diff --
    
    fxied


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216734107
  
    @rhtyd I am trying to understand how reordering should work.   User creates a role with rules ordered as follows:
    
    1. Rule A
    1. Rule B
    1. Rule C
    1. Rule D
    1. Rule E
    1. Rule F
    
    When the user creates this role, the API guarantees that these six rules and their order will be persisted atomically.  Later, the user wants to reorder the rules as follows:
    
    1. Rule A
    1. Rule D
    1. Rule B
    1. Rule C
    1. Rule E
    1. Rule F
    
    As I understand the API, the following discrete API calls are required to affect this change:
    
    1. ```updateRolePermission(roleId: "Rule D", parentUUID: "Rule A")```
    1. ```updateRolePermission(roleId: "Rule B", parentUUID: "Rule D")```
    1. ```updateRolePermission(roleId: "Rule C", parentUUID: "Rule B")```
    
    Assuming my understanding regarding the API usage is correct, I have the following concerns about this approach:
    
    1. **Operation Interleaving**: Since each API call can only provide a guarantee the persistence of a single list mutation, it is possible for two users to change the order of rules for permission simultaneously.  The result of this operation interleaving would leave the order of the rules in an indeterminate order.  
    1. **Client Complexity**: It places a burden on clients to track the mutations to the list of rules rather than simply tracking the current order of the list.  This additional complexity could lead to more error prone client code.
    
    As a user my expectation would be to reorder all rules associated with a permission in one atomic operation rather than a series of atomic mutation operations.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220241843
  
    @swill I had a GTM with Anshul to figure out the issue, his environment is not clean and I've asked him to setup a new environment (effectively a new dev box). I've shared notes with him that if `commands.properties` file is present anywhere on the classpath with the dynamic feature enabled would cause issues as requirement for dynamic roles feature to work is that the commands.properties file does not exist on the classpath.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by anshul1886 <gi...@git.apache.org>.
Github user anshul1886 commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220238146
  
    dev machine which I am using for last 2-3 years


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by anshul1886 <gi...@git.apache.org>.
Github user anshul1886 commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-219980293
  
    @rhtyd There are many APIs. I have not prepared any list. But creating setup from scratch should reproduce it. Some operations in which it can be observed
    
    1. ListAffinity groups
    2. Create secondary storage
    



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by anshul1886 <gi...@git.apache.org>.
Github user anshul1886 commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-219990167
  
    @rhtyd I have rebuilt my branch which was rebased yesterday. I am creating new setup. There is no new API introduced in my branch. I am talking about existing APIs which are failing.
    
    After putting commands.properties I am not facing issues. But as you pointed that is not what is intended with this PR.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216603249
  
    @swill I'm fine with all changes, I've ran a final set of tests as well. @jburwell please share any outstanding issue that should be fixed, @borisstoyanov and I are LGTM on this


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220090191
  
    @swill Travis and your CI are passing with this feature during which a data center is deployed and all sorts of APIs are called so it's likely an env issue specific for @anshul1886. We've put a lot of testing effort on this including various upgrade scenarios, and have seen huge amount of effort on code review so it is not needed to revert the feature; though if there is a valid bug it ought to be fixed and that's what freeze is about -- fixing bugs.
    
    Here what I'm speculating where it is failing:
    Developer sets up CloudStack with a db schema prior to when RBAC was merged. Then, the developer rebuilds CloudStack but does not redeploy a fresh/clean db, and commands.properties.in is removed so his static-checker won't work. The root admin with role_id=1 is by default allowed all APIs in the dynamic checker so the feature ensures that root-admin is never locked out of the system. The developers ought to cleanly rebuild CloudStack and re-deploy a fresh db after a rebase in their feature branch or on master.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-218358868
  
    @swill done, though if it fails again we may ignore that as long as travis is green


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-219953009
  
    @anshul1886 can you share the details, which APIs. Also, are you facing this on master or on your personal/feature branch?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220037863
  
    If we have to revert this, it will be a mess.  Lets try to get to the bottom of this ASAP as the freeze is basically here...


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by borisstoyanov <gi...@git.apache.org>.
Github user borisstoyanov commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214724784
  
    LGTM. :+1:


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by anshul1886 <gi...@git.apache.org>.
Github user anshul1886 commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220237502
  
    @rhtyd Default value for dynamic.apichecker.enabled is true. role_id for default account is 1.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215171740
  
    @koushik-das I don't understand how adoption of this feature differs from any other feature.  A user deploys the release into a test/pilot environment and determine whether or not it they wish to migrate to it.  To be clear, existing users are **not** automatically migrated.
    
    Per the FS, this feature alows the definition of custom roles as you describe.  For backwards compatibility, all roles must be mapped to one of the four types.  
    
    I don't understand your point about someone writing a plugin to allow custom roles.  This PR is exactly the plugin you describe.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60570949
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,170 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import org.apache.log4j.Logger;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    protected static final Logger LOGGER = Logger.getLogger(DynamicRoleBasedAPIAccessChecker.class);
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null || permissions.isEmpty() || Strings.isNullOrEmpty(commandName)) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            try {
    +                final Rule rule = new Rule(permission.getRule());
    +                if (rule.matches(commandName)) {
    +                    return true;
    +                }
    +            } catch (InvalidParameterValueException e) {
    +                LOGGER.warn("Invalid rule permission, please fix id=" + permission.getId() + " rule=" + permission.getRule());
    +                continue;
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    +            return true;
    +        }
    +
    +        // Check for deny rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.DENY, commandName)) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Check annotations
    +        if (annotationRoleBasedApisMap.get(accountRole.getRoleType()) != null
    +                && annotationRoleBasedApisMap.get(accountRole.getRoleType()).contains(commandName)) {
    +            return true;
    +        }
    +
    +        denyApiAccess(commandName);
    +        return false;
    +    }
    +
    +    public void addApiToRoleBasedAnnotationsMap(final RoleType roleType, final String commandName) {
    +        if (roleType == null || Strings.isNullOrEmpty(commandName)) {
    +            return;
    +        }
    +        final Set<String> commands = annotationRoleBasedApisMap.get(roleType);
    +        if (commands != null && !commands.contains(commandName)) {
    +            commands.add(commandName);
    +        }
    +    }
    +
    +    @Override
    +    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
    +        super.configure(name, params);
    +        return true;
    +    }
    +
    +    @Override
    +    public boolean start() {
    +        for (PluggableService service : services) {
    +            for (Class<?> clz : service.getCommands()) {
    +                APICommand command = clz.getAnnotation(APICommand.class);
    +                for (RoleType role : command.authorized()) {
    +                    addApiToRoleBasedAnnotationsMap(role, command.name());
    --- End diff --
    
    @bhaisaab I didn't find any APIs using this authorized field. So can this be ignored from the checks as backward compatibility won't be impacted. In that case the permissions model can be simplified as I had mentioned in a previous comment. The current way of evaluating the check looks complicated and may be difficult for the admin to interpret. What do you think?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by pdube <gi...@git.apache.org>.
Github user pdube commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59812425
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleType.java ---
    @@ -16,18 +16,90 @@
     // under the License.
     package org.apache.cloudstack.acl;
     
    +import com.cloud.user.Account;
    +import com.google.common.base.Enums;
    +import com.google.common.base.Strings;
    +
     // Enum for default roles in CloudStack
     public enum RoleType {
    -    Admin(1), ResourceAdmin(2), DomainAdmin(4), User(8), Unknown(0);
    +    Admin(1L, Account.ACCOUNT_TYPE_ADMIN, 1),
    +    ResourceAdmin(2L, Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN, 2),
    +    DomainAdmin(3L, Account.ACCOUNT_TYPE_DOMAIN_ADMIN, 4),
    +    User(4L, Account.ACCOUNT_TYPE_NORMAL, 8),
    +    Unknown(-1L, (short) -1, 0);
     
    +    private long id;
    +    private short accountType;
         private int mask;
     
    -    private RoleType(int mask) {
    +    RoleType(final long id, final short accountType, final int mask) {
    +        this.id = id;
    +        this.accountType = accountType;
             this.mask = mask;
         }
     
    -    public int getValue() {
    +    public long getId() {
    +        return id;
    +    }
    +
    +    public short getAccountType() {
    +        return accountType;
    +    }
    +
    +    public int getMask() {
             return mask;
         }
    -}
     
    +    public static RoleType fromString(final String name) {
    +        if (!Strings.isNullOrEmpty(name)
    +                && Enums.getIfPresent(RoleType.class, name).isPresent()) {
    +            return RoleType.valueOf(name);
    +        }
    +        return null;
    +    }
    +
    +    public static RoleType fromMask(int mask) {
    +        for (RoleType roleType : RoleType.values()) {
    +            if (roleType.getMask() == mask) {
    +                return roleType;
    +            }
    +        }
    +        return Unknown;
    +    }
    +
    +    public static RoleType getByAccountType(final short accountType) {
    +        RoleType roleType = RoleType.Unknown;
    +        switch (accountType) {
    +            case Account.ACCOUNT_TYPE_ADMIN:
    +                roleType = RoleType.Admin;
    +                break;
    +            case Account.ACCOUNT_TYPE_DOMAIN_ADMIN:
    +                roleType = RoleType.DomainAdmin;
    +                break;
    +            case Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN:
    +                roleType = RoleType.ResourceAdmin;
    --- End diff --
    
    Thanks. I had seen it in the code and looked for more information about it, but didn't find anything conclusive


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59852381
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleService.java ---
    @@ -0,0 +1,43 @@
    +// 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.cloudstack.acl;
    +
    +import org.apache.cloudstack.framework.config.ConfigKey;
    +
    +import java.util.List;
    +
    +public interface RoleService {
    +
    +    ConfigKey<Boolean> EnableDynamicApiChecker = new ConfigKey<>("Hidden", Boolean.class, "dynamic.apichecker.enabled", "false",
    +            "If set to true, this enables the dynamic role-based api access checker and disables the default static role-based api access checker.",
    +            true);
    +
    +    boolean isEnabled();
    +    Role findRole(final Long id);
    +    Role createRole(final String name, final RoleType roleType, final String description);
    +    boolean updateRole(final Role role, final String name, final RoleType roleType, final String description);
    +    boolean deleteRole(final Role role);
    +
    +    RolePermission findRolePermission(final Long id);
    +    RolePermission createRolePermission(final Role role, final Rule rule, final RolePermission.Permission permission, final String description);
    +    boolean updateRolePermission(final RolePermission rolePermission, final Rule rule, final RolePermission.Permission permission, final String description);
    +    boolean deleteRolePermission(final RolePermission rolePermission);
    +
    +    List<Role> findAllRolesBy(final Long id, final String name, final RoleType roleType);
    --- End diff --
    
    Thanks fixed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by mlsorensen <gi...@git.apache.org>.
Github user mlsorensen commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214843061
  
    It seems complicated to try to actively promote a choice of multiple ways to check for API authorization (dynamic vs static). I am for deprecating commands.properties, and honestly I wish it would have been removed from the source tree altogether long ago instead of informally deprecated, because people have been adding to it rather than using annotations. I've spoken to committers even recently who didn't realize that you didn't have to use commands.properties. The annotation and dymanic methods are so much cleaner, particularly for plugin developers as it's often the only piece of the source tree that they have to change outside of their own plugin directory.
    
    I don't really think the majority of users will care about the implementation, so long as functionally the result is the same. That is to say that the API permissions on a fresh install before and after this is merged should behave the same out of the box. If that's the case then I don't think users will feel forced into anything or even notice that something was changed, and if someone really does care, it sounds simple enough to enable commands.properties. If someone is used to fiddling with this file, they won't have difficulty creating it and toggling a global setting manually. Speaking from having the experience of trying to manage a custom commands.properties, the work involved here to choose static is minor compared to simply maintaining a custom commands.properties, making sure an upgrade doesn't overwrite your change, and merging in new changes.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59785020
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleType.java ---
    @@ -16,18 +16,90 @@
     // under the License.
     package org.apache.cloudstack.acl;
     
    +import com.cloud.user.Account;
    +import com.google.common.base.Enums;
    +import com.google.common.base.Strings;
    +
     // Enum for default roles in CloudStack
     public enum RoleType {
    -    Admin(1), ResourceAdmin(2), DomainAdmin(4), User(8), Unknown(0);
    +    Admin(1L, Account.ACCOUNT_TYPE_ADMIN, 1),
    +    ResourceAdmin(2L, Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN, 2),
    +    DomainAdmin(3L, Account.ACCOUNT_TYPE_DOMAIN_ADMIN, 4),
    +    User(4L, Account.ACCOUNT_TYPE_NORMAL, 8),
    +    Unknown(-1L, (short) -1, 0);
     
    +    private long id;
    +    private short accountType;
         private int mask;
     
    -    private RoleType(int mask) {
    +    RoleType(final long id, final short accountType, final int mask) {
    +        this.id = id;
    +        this.accountType = accountType;
             this.mask = mask;
         }
     
    -    public int getValue() {
    +    public long getId() {
    +        return id;
    +    }
    +
    +    public short getAccountType() {
    +        return accountType;
    +    }
    +
    +    public int getMask() {
             return mask;
         }
    -}
     
    +    public static RoleType fromString(final String name) {
    +        if (!Strings.isNullOrEmpty(name)
    +                && Enums.getIfPresent(RoleType.class, name).isPresent()) {
    +            return RoleType.valueOf(name);
    +        }
    +        return null;
    +    }
    +
    +    public static RoleType fromMask(int mask) {
    +        for (RoleType roleType : RoleType.values()) {
    +            if (roleType.getMask() == mask) {
    +                return roleType;
    +            }
    +        }
    +        return Unknown;
    +    }
    +
    +    public static RoleType getByAccountType(final short accountType) {
    +        RoleType roleType = RoleType.Unknown;
    +        switch (accountType) {
    +            case Account.ACCOUNT_TYPE_ADMIN:
    +                roleType = RoleType.Admin;
    +                break;
    +            case Account.ACCOUNT_TYPE_DOMAIN_ADMIN:
    +                roleType = RoleType.DomainAdmin;
    +                break;
    +            case Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN:
    +                roleType = RoleType.ResourceAdmin;
    --- End diff --
    
    @pdube I think this was used to support cpbm-cloudstack integration, I don't know any documentation; please google? For backward compatibility reasons, all four roles types supported by static-role-based-checked (as in commands.properties) need to be supported.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60880231
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    +        return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10))
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_list(self):
    +        """
    +            Tests that default four roles exist
    +        """
    +        roleTypes = {1: "Admin", 2: "ResourceAdmin", 3: "DomainAdmin", 4: "User"}
    +        for idx in range(1,5):
    +            list_roles = Role.list(self.apiclient, id=idx)
    +            self.assertEqual(
    +                isinstance(list_roles, list),
    +                True,
    +                "List Roles response was not a valid list"
    +            )
    +            self.assertEqual(
    +                len(list_roles),
    +                1,
    +                "List Roles response size was not 1"
    +            )
    +            self.assertEqual(
    +                list_roles[0].type,
    +                roleTypes[idx],
    +                msg="Default role type differs from expectation"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_create(self):
    +        """
    +            Tests normal lifecycle operations for roles
    +        """
    +        # Reuse self.role created in setUp()
    +        try:
    +            role = Role.create(
    +                self.apiclient,
    +                self.testdata["role"]
    +            )
    +            self.fail("An exception was expected when creating duplicate roles")
    +        except CloudstackAPIException: pass
    +
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_roles, list),
    +            True,
    +            "List Roles response was not a valid list"
    +        )
    +        self.assertEqual(
    +            len(list_roles),
    +            1,
    +            "List Roles response size was not 1"
    +        )
    +        self.assertEqual(
    +            list_roles[0].name,
    +            self.testdata["role"]["name"],
    +            msg="Role name does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_roles[0].type,
    +            self.testdata["role"]["type"],
    +            msg="Role type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +        self.assertEqual(
    +            update_role.type,
    +            'Admin',
    +            msg="Role type does not match updated role type"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update_role_inuse(self):
    +        """
    +            Tests role update when role is in use by an account
    +        """
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    --- End diff --
    
    Fixed.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-213693730
  
    I did some basic tests yesterday. Some comments based on that. Will do some more testing next week.
    - I see that commands.properties is removed altogether and the data migrated to DB. What are the perf. implications for this even when using the static checker? Earlier the contents were read from file on MS startup and stored in memory as hash map, now for every API call there is a DB query for access check.
    - Since the data is automatically migrated from commands.properties file to DB (in new format), how are you ensuring that the data consistency is maintained?
    - For dev setup, dynamic roles configuration is enabled. I was able to perform operations using dynamic role checker. But when I switched back to static role checker things stopped working. It wasn't allowing me to login from UI. Can you check?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59800725
  
    --- Diff: api/test/org/apache/cloudstack/acl/RoleTypeTest.java ---
    @@ -0,0 +1,92 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.user.Account;
    +import org.junit.Assert;
    +import org.junit.Test;
    +import org.mockito.Mockito;
    +
    +import java.util.Arrays;
    +
    +public class RoleTypeTest {
    +
    +    @Test
    +    public void testRoleTypeFromString() {
    +        Assert.assertEquals(RoleType.fromString(null), null);
    +        Assert.assertEquals(RoleType.fromString(""), null);
    +        Assert.assertEquals(RoleType.fromString("admin"), null);
    +        Assert.assertEquals(RoleType.fromString("12345%^&*"), null);
    +        for (RoleType roleType : RoleType.values()) {
    +            Assert.assertEquals(RoleType.fromString(roleType.name()), roleType);
    +        }
    +    }
    +
    +    @Test
    +    public void testDefaultRoleMaskByValue() {
    +        Assert.assertEquals(RoleType.fromMask(1), RoleType.Admin);
    +        Assert.assertEquals(RoleType.fromMask(2), RoleType.ResourceAdmin);
    +        Assert.assertEquals(RoleType.fromMask(4), RoleType.DomainAdmin);
    +        Assert.assertEquals(RoleType.fromMask(8), RoleType.User);
    +        Assert.assertEquals(RoleType.fromMask(0), RoleType.Unknown);
    +    }
    +
    +    @Test
    +    public void testGetByAccountType() {
    +        Assert.assertEquals(RoleType.getByAccountType(Account.ACCOUNT_TYPE_NORMAL), RoleType.User);
    +        Assert.assertEquals(RoleType.getByAccountType(Account.ACCOUNT_TYPE_ADMIN), RoleType.Admin);
    +        Assert.assertEquals(RoleType.getByAccountType(Account.ACCOUNT_TYPE_DOMAIN_ADMIN), RoleType.DomainAdmin);
    +        Assert.assertEquals(RoleType.getByAccountType(Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN), RoleType.ResourceAdmin);
    +        Assert.assertEquals(RoleType.getByAccountType(Account.ACCOUNT_TYPE_PROJECT), RoleType.Unknown);
    +    }
    +
    +    @Test
    +    public void testGetRoleByAccountTypeWhenRoleIdIsProvided() {
    +        Assert.assertEquals(RoleType.getRoleByAccountType(123L, Account.ACCOUNT_TYPE_ADMIN), Long.valueOf(123L));
    +        Assert.assertEquals(RoleType.getRoleByAccountType(1234L, null), Long.valueOf(1234L));
    +    }
    +
    +    @Test
    +    public void testGetRoleByAccountTypeForDefaultAccountTypes() {
    +        Assert.assertEquals(RoleType.getRoleByAccountType(null, Account.ACCOUNT_TYPE_ADMIN), (Long) RoleType.Admin.getId());
    --- End diff --
    
    Passing ``null`` as a parameter to a method as a flag is an anti-pattern.  Create a two parameter override of ``getRoleByAccountType`` to cover this scenario.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60882026
  
    --- Diff: plugins/user-authenticators/ldap/src/org/apache/cloudstack/api/command/LdapCreateAccountCmd.java ---
    @@ -119,6 +131,9 @@ private Long getDomainId() {
     
         @Override
         public void execute() throws ServerApiException {
    +        if (getAccountType() == null && getRoleId() == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Both account type and role ID are not provided");
    --- End diff --
    
    @jburwell before this changes `accountType` was required, now it's made optional with introduction of roleId, this check is to ensure at least one of the two are provided.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-217456452
  
    I will get this CI'ed.  I have 4 new CI boxes in play now, but I am trying to figure out how to stabilize master, so I will run this as soon as I get master sorted out.  Sorry for the delay...


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220237753
  
    @anshul1886 is this environment installed using rpms/deb or it's a developer machine with Ubuntu 12.04? Would you prefer a GTM to get to the bottom of the issue, instead of back and forth on PR/ML?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-216441837
  
    Thanks @rhtyd, this makes me feel a little more comfortable.  @borisroman if you can give us a bit of an outline of what you have tested, I think it will help everyone in this thread with the confidence in this PR.  I am pretty confident, but at the same time, we can't be too careful when introducing functionality that can potentially block users.  I really appreciate all the work you guys have been doing on both the development front and with testing and validation. \U0001f44d 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60744698
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    +        return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10))
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_list(self):
    +        """
    +            Tests that default four roles exist
    +        """
    +        roleTypes = {1: "Admin", 2: "ResourceAdmin", 3: "DomainAdmin", 4: "User"}
    +        for idx in range(1,5):
    +            list_roles = Role.list(self.apiclient, id=idx)
    +            self.assertEqual(
    +                isinstance(list_roles, list),
    +                True,
    +                "List Roles response was not a valid list"
    +            )
    +            self.assertEqual(
    +                len(list_roles),
    +                1,
    +                "List Roles response size was not 1"
    +            )
    +            self.assertEqual(
    +                list_roles[0].type,
    +                roleTypes[idx],
    +                msg="Default role type differs from expectation"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_create(self):
    +        """
    +            Tests normal lifecycle operations for roles
    +        """
    +        # Reuse self.role created in setUp()
    +        try:
    +            role = Role.create(
    +                self.apiclient,
    +                self.testdata["role"]
    +            )
    +            self.fail("An exception was expected when creating duplicate roles")
    +        except CloudstackAPIException: pass
    +
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_roles, list),
    +            True,
    +            "List Roles response was not a valid list"
    +        )
    +        self.assertEqual(
    +            len(list_roles),
    +            1,
    +            "List Roles response size was not 1"
    +        )
    +        self.assertEqual(
    +            list_roles[0].name,
    +            self.testdata["role"]["name"],
    +            msg="Role name does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_roles[0].type,
    +            self.testdata["role"]["type"],
    +            msg="Role type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +        self.assertEqual(
    +            update_role.type,
    +            'Admin',
    +            msg="Role type does not match updated role type"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update_role_inuse(self):
    +        """
    +            Tests role update when role is in use by an account
    +        """
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        try:
    +            self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +            self.fail("Updation of role type is not allowed when role is in use")
    +        except CloudstackAPIException: pass
    +
    +        self.role.update(self.apiclient, name=new_role_name)
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_delete(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        self.role.delete(self.apiclient)
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            list_roles,
    +            None,
    +            "List Roles response should be empty"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_inuse_deletion(self):
    +        """
    +            Test to ensure role in use cannot be deleted
    +        """
    +        try:
    +            self.role.delete(self.apiclient)
    +            self.fail("Role with any account should not be allowed to be deleted")
    +        except CloudstackAPIException: pass
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_default_role_deletion(self):
    +        """
    +            Test to ensure 4 default roles cannot be deleted
    +        """
    +        for idx in range(1,5):
    +            cmd = deleteRole.deleteRoleCmd()
    +            cmd.id = idx
    +            try:
    +                self.apiclient.deleteRole(cmd)
    +                self.fail("Default role got deleted with id: " + idx)
    +            except CloudstackAPIException: pass
    --- End diff --
    
    Is there any value asserting on the contents of the error message?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60880213
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    +        return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10))
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_list(self):
    +        """
    +            Tests that default four roles exist
    +        """
    +        roleTypes = {1: "Admin", 2: "ResourceAdmin", 3: "DomainAdmin", 4: "User"}
    +        for idx in range(1,5):
    +            list_roles = Role.list(self.apiclient, id=idx)
    +            self.assertEqual(
    +                isinstance(list_roles, list),
    +                True,
    +                "List Roles response was not a valid list"
    +            )
    +            self.assertEqual(
    +                len(list_roles),
    +                1,
    +                "List Roles response size was not 1"
    +            )
    +            self.assertEqual(
    +                list_roles[0].type,
    +                roleTypes[idx],
    +                msg="Default role type differs from expectation"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_create(self):
    +        """
    +            Tests normal lifecycle operations for roles
    +        """
    +        # Reuse self.role created in setUp()
    +        try:
    +            role = Role.create(
    +                self.apiclient,
    +                self.testdata["role"]
    +            )
    +            self.fail("An exception was expected when creating duplicate roles")
    +        except CloudstackAPIException: pass
    +
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_roles, list),
    +            True,
    +            "List Roles response was not a valid list"
    +        )
    +        self.assertEqual(
    +            len(list_roles),
    +            1,
    +            "List Roles response size was not 1"
    +        )
    +        self.assertEqual(
    +            list_roles[0].name,
    +            self.testdata["role"]["name"],
    +            msg="Role name does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_roles[0].type,
    +            self.testdata["role"]["type"],
    +            msg="Role type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +        self.assertEqual(
    +            update_role.type,
    +            'Admin',
    +            msg="Role type does not match updated role type"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update_role_inuse(self):
    +        """
    +            Tests role update when role is in use by an account
    +        """
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        try:
    +            self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +            self.fail("Updation of role type is not allowed when role is in use")
    +        except CloudstackAPIException: pass
    +
    +        self.role.update(self.apiclient, name=new_role_name)
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_delete(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        self.role.delete(self.apiclient)
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            list_roles,
    +            None,
    +            "List Roles response should be empty"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_inuse_deletion(self):
    +        """
    +            Test to ensure role in use cannot be deleted
    +        """
    +        try:
    +            self.role.delete(self.apiclient)
    +            self.fail("Role with any account should not be allowed to be deleted")
    +        except CloudstackAPIException: pass
    --- End diff --
    
    We can do that but since we've catching any ACS server api exception caught by CloudStackAPIException, we know it's ACS specific API exception and not any HTTP exception.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60738644
  
    --- Diff: scripts/util/migrate-dynamicroles.py ---
    @@ -0,0 +1,134 @@
    +#!/usr/bin/python
    +# -*- coding: utf-8 -*-
    +# 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.
    +
    +import os
    +import sys
    +import uuid
    +
    +from contextlib import closing
    +from optparse import OptionParser
    +
    +try:
    +    import MySQLdb
    +except ImportError:
    +    print("MySQLdb cannot be imported, please install python-mysqldb(apt) or mysql-python(yum)")
    +    sys.exit(1)
    +
    +dryrun = False
    +
    +
    +def runSql(conn, query):
    +    if dryrun:
    +        print("Running SQL query: " + query)
    +        return
    +    with closing(conn.cursor()) as cursor:
    +        cursor.execute(query)
    +
    +
    +def migrateApiRolePermissions(apis, conn):
    +    # All allow for root admin role Admin(id:1)
    +    runSql(conn, "INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), 1, '*', 'Allow');")
    +    # Migrate rules based on commands.properties rule for ResourceAdmin(id:2), DomainAdmin(id:3), User(id:4)
    +    octetKey = {2:2, 3:4, 4:8}
    +    for role in [2, 3, 4]:
    +        for api in sorted(apis.keys()):
    +            # Ignore auth commands
    +            if api in ['login', 'logout', 'samlSso', 'samlSlo', 'listIdps', 'listAndSwitchSamlAccount', 'getSPMetadata']:
    +                continue
    +            if (octetKey[role] & int(apis[api])) > 0:
    +                runSql(conn, "INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'Allow');" % (role, api))
    +
    +
    +def main():
    +    parser = OptionParser()
    +    parser.add_option("-b", "--db", action="store", type="string", dest="db", default="cloud",
    +                        help="The name of the database, default: cloud")
    +    parser.add_option("-u", "--user", action="store", type="string", dest="user", default="cloud",
    +                        help="User name a MySQL user with privileges on cloud database")
    +    parser.add_option("-p", "--password", action="store", type="string", dest="password", default="cloud",
    +                        help="Password of a MySQL user with privileges on cloud database")
    +    parser.add_option("-H", "--host", action="store", type="string", dest="host", default="127.0.0.1",
    +                        help="Host or IP of the MySQL server")
    +    parser.add_option("-P", "--port", action="store", type="int", dest="port", default=3306,
    +                        help="Host or IP of the MySQL server")
    +    parser.add_option("-f", "--properties-file", action="store", type="string", dest="commandsfile", default="/etc/cloudstack/management/commands.properties",
    +                        help="The commands.properties file")
    +    parser.add_option("-d", "--dryrun", action="store_true", dest="dryrun", default=False,
    +                        help="Dry run and debug operations this tool will perform")
    +    (options, args) = parser.parse_args()
    +
    +    print("Apache CloudStack Role Permission Migration Tool")
    +    print("(c) Apache CloudStack Authors and the ASF, under the Apache License, Version 2.0\n")
    +
    +    global dryrun
    +    if options.dryrun:
    +        dryrun = True
    +
    +    conn = MySQLdb.connect(
    +            host=options.host,
    +            user=options.user,
    +            passwd=options.password,
    +            port=int(options.port),
    +            db=options.db)
    +
    +    if not os.path.isfile(options.commandsfile):
    +        print("Provided commands.properties cannot be accessed or does not exist, please check check permissions")
    +        sys.exit(1)
    +
    +    while True:
    +        choice = raw_input("Running this migration tool will remove any " +
    +                           "default-role rules in cloud.role_permissions. " +
    +                           "Do you want to continue? [y/N]").lower()
    +        if choice == 'y':
    +            break
    +        else:
    +            print("Aborting!")
    +            sys.exit(1)
    +
    +    # Generate API to permission octet map
    +    apiMap = {}
    +    with open(options.commandsfile) as f:
    +        for line in f.readlines():
    +            if not line or line == '' or line == '\n' or line.startswith('#'):
    +                continue
    +            name, value = line.split('=')
    +            apiMap[name.strip()] = value.strip()
    +
    +    # Rename and deprecate old commands.properties file
    +    if not dryrun:
    +        os.rename(options.commandsfile, options.commandsfile + '.deprecated')
    +    print("The commands.properties file has been deprecated and moved at: " + options.commandsfile + '.deprecated')
    +
    +    # Truncate any rules in cloud.role_permissions table
    +    runSql(conn, "DELETE FROM `cloud`.`role_permissions` WHERE `role_id` in (1,2,3,4);")
    +
    +    # Migrate rules from commands.properties to cloud.role_permissions
    +    migrateApiRolePermissions(apiMap, conn)
    +    print("Static role permissions from commands.properties have been migrated into the db")
    +
    +    # Enable dynamic role based API checker
    +    runSql(conn, "UPDATE `cloud`.`configuration` SET value='true' where name='dynamic.apichecker.enabled'")
    +    conn.commit()
    --- End diff --
    
    Why aren't we catching any potential errors and reporting them to the use?  Also, why aren't we issuing an explicit rollback when an exception occurs?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59855085
  
    --- Diff: api/src/org/apache/cloudstack/acl/Rule.java ---
    @@ -0,0 +1,42 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.google.common.base.Strings;
    +
    +public final class Rule {
    +    private final String rule;
    +    private final static String ALLOWED_PATTERN = "^[a-zA-Z0-9*]+$";
    --- End diff --
    
    No, we allow wildcards at any position


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60744905
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    +        return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10))
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_list(self):
    +        """
    +            Tests that default four roles exist
    +        """
    +        roleTypes = {1: "Admin", 2: "ResourceAdmin", 3: "DomainAdmin", 4: "User"}
    +        for idx in range(1,5):
    +            list_roles = Role.list(self.apiclient, id=idx)
    +            self.assertEqual(
    +                isinstance(list_roles, list),
    +                True,
    +                "List Roles response was not a valid list"
    +            )
    +            self.assertEqual(
    +                len(list_roles),
    +                1,
    +                "List Roles response size was not 1"
    +            )
    +            self.assertEqual(
    +                list_roles[0].type,
    +                roleTypes[idx],
    +                msg="Default role type differs from expectation"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_create(self):
    +        """
    +            Tests normal lifecycle operations for roles
    +        """
    +        # Reuse self.role created in setUp()
    +        try:
    +            role = Role.create(
    +                self.apiclient,
    +                self.testdata["role"]
    +            )
    +            self.fail("An exception was expected when creating duplicate roles")
    +        except CloudstackAPIException: pass
    +
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_roles, list),
    +            True,
    +            "List Roles response was not a valid list"
    +        )
    +        self.assertEqual(
    +            len(list_roles),
    +            1,
    +            "List Roles response size was not 1"
    +        )
    +        self.assertEqual(
    +            list_roles[0].name,
    +            self.testdata["role"]["name"],
    +            msg="Role name does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_roles[0].type,
    +            self.testdata["role"]["type"],
    +            msg="Role type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +        self.assertEqual(
    +            update_role.type,
    +            'Admin',
    +            msg="Role type does not match updated role type"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update_role_inuse(self):
    +        """
    +            Tests role update when role is in use by an account
    +        """
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        try:
    +            self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +            self.fail("Updation of role type is not allowed when role is in use")
    +        except CloudstackAPIException: pass
    +
    +        self.role.update(self.apiclient, name=new_role_name)
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_delete(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        self.role.delete(self.apiclient)
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            list_roles,
    +            None,
    +            "List Roles response should be empty"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_inuse_deletion(self):
    +        """
    +            Test to ensure role in use cannot be deleted
    +        """
    +        try:
    +            self.role.delete(self.apiclient)
    +            self.fail("Role with any account should not be allowed to be deleted")
    +        except CloudstackAPIException: pass
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_default_role_deletion(self):
    +        """
    +            Test to ensure 4 default roles cannot be deleted
    +        """
    +        for idx in range(1,5):
    +            cmd = deleteRole.deleteRoleCmd()
    +            cmd.id = idx
    +            try:
    +                self.apiclient.deleteRole(cmd)
    +                self.fail("Default role got deleted with id: " + idx)
    +            except CloudstackAPIException: pass
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_list(self):
    +        """
    +            Tests listing of default role's permission
    +        """
    +        for idx in range(1,5):
    +            list_rolepermissions = RolePermission.list(self.apiclient, roleid=idx)
    +            self.assertEqual(
    +                isinstance(list_rolepermissions, list),
    +                True,
    +                "List rolepermissions response was not a valid list"
    +            )
    +            self.assertTrue(
    +                len(list_rolepermissions) > 0,
    +                "List rolepermissions response was empty"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_create(self):
    +        """
    +            Tests creation of role permission
    +        """
    +        # Reuse self.rolepermission created in setUp()
    +        try:
    +            rolepermission = RolePermission.create(
    +                self.apiclient,
    +                self.testdata["rolepermission"]
    +            )
    +            self.fail("An exception was expected when creating duplicate role permissions")
    +        except CloudstackAPIException: pass
    +
    +        list_rolepermissions = RolePermission.list(self.apiclient, roleid=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_rolepermissions, list),
    +            True,
    +            "List rolepermissions response was not a valid list"
    +        )
    +        self.assertNotEqual(
    +            len(list_rolepermissions),
    +            0,
    +            "List rolepermissions response was empty"
    +        )
    +        self.assertEqual(
    +            list_rolepermissions[0].rule,
    +            self.testdata["rolepermission"]["rule"],
    +            msg="Role permission rule does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_rolepermissions[0].permission,
    +            self.testdata["rolepermission"]["permission"],
    +            msg="Role permission permission-type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_update(self):
    +        """
    +            Tests updation of role permission
    +        """
    +        new_rule = "some*Rule" + self.getRandomString()
    +        self.rolepermission.update(self.apiclient, rule=new_rule, permission='deny')
    +        updated_rolepermission = filter(lambda p: p.id == self.rolepermission.id,
    +                                        RolePermission.list(self.apiclient, roleid=self.role.id))[0]
    +        self.assertEqual(
    +            updated_rolepermission.rule,
    +            new_rule,
    +            msg="Role permission rule does not match updated role permission"
    +        )
    +        self.assertEqual(
    +            updated_rolepermission.permission,
    +            'deny',
    +            msg="Role permission permission-type does not match updated role permission"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_rolepermission_lifecycle_update_invalid_rule(self):
    +        """
    +            Tests updation of role permission with an invalid rule
    +        """
    +        for badrule in [" ", "^delete.*$", "some-@p1-!"]:
    +            try:
    +                self.rolepermission.update(self.apiclient, rule=badrule, permission='deny')
    +                self.fail("Invalid role permission rule got updated")
    +            except CloudstackAPIException: pass
    --- End diff --
    
    Is there any value asserting on the contents of the error message?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60365855
  
    --- Diff: engine/schema/src/org/apache/cloudstack/acl/RolePermissionVO.java ---
    @@ -0,0 +1,109 @@
    +// 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.cloudstack.acl;
    +
    +import javax.persistence.Column;
    +import javax.persistence.Entity;
    +import javax.persistence.EnumType;
    +import javax.persistence.Enumerated;
    +import javax.persistence.GeneratedValue;
    +import javax.persistence.GenerationType;
    +import javax.persistence.Id;
    +import javax.persistence.Table;
    +import java.util.UUID;
    +
    +@Entity
    +@Table(name = "role_permissions")
    +public class RolePermissionVO implements RolePermission {
    --- End diff --
    
    @bhaisaab 
    >> I'm not sure what you mean by Role refers RolePermissions, as of now each role has a list of role permissions linked to it if that's what you're asking.
    With the containment approach, if there is a need to add a permission to allow say deployVM API on every role then you need to create a permission for every such role. This will lead to data duplication. Instead if the permission is created only once and then mapped to all the roles that needs it duplication won't happen. This is what I meant as reference relationship. Check the cloudstack DB with table name ending in map t o get an idea. This will also mean changing the APIs to reflect the same.



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-211923768
  
    @swill okay I'll check this out and see if I can reproduce the failure. I'll keep you posted.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59801500
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    +
    +                } catch (SQLException e) {
    +                    s_logger.error("Failed to update cloud.account role_id for account id:" + accountId + " with exception: " + e.getMessage());
    +                    throw new CloudRuntimeException("Exception while updating cloud.account role_id", e);
    +                }
    +            }
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Exception while migrating existing account table's role_id column to a role based on account type", e);
    +        }
    +        s_logger.debug("Done migrating existing accounts to use one of default roles based on account type");
    +    }
    +
    +    private void setupRolesAndPermissionsForDynamicRBAC(final Connection conn) {
    +        try ( PreparedStatement selectStatement = conn.prepareStatement("SELECT * FROM `cloud`.`roles`");
    +              ResultSet resultSet = selectStatement.executeQuery()) {
    --- End diff --
    
    ``resultSet`` is a resource that needs to closed.  Please add it to the enclosing try with resources block.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by koushik-das <gi...@git.apache.org>.
Github user koushik-das commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214269005
  
    @rhtyd Have you ran the tests using a regular user? As per dynamic checker code, for root admin all checks are bypassed. I think a good comparison will be to run the tests on master with and without dynamic role checker using a regular user so that the DB code path gets exercised.
    
    So are you saying static role checker will no longer be used and the migration of data from commands.properties to DB is forced. Rather than completely removing commands.properties, it will be good to keep both - static checker will only use commands.properties and dynamic checker only goes to DB. The choice should be left with users when they want to migrate.



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59801544
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    +            while (selectResultSet.next()) {
    +                Long accountId = selectResultSet.getLong(1);
    +                Short accountType = selectResultSet.getShort(2);
    +                Long roleId = null;
    +                for (RoleType roleType : roleTypes) {
    +                    if (roleType.getAccountType() == accountType) {
    +                        roleId = roleType.getId();
    +                        break;
    +                    }
    +                }
    +                if (roleId == null) {
    +                    continue;
    +                }
    +                try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`account` SET role_id = ? WHERE id = ?;")) {
    +                    updateStatement.setLong(1, roleId);
    +                    updateStatement.setLong(2, accountId);
    +                    updateStatement.executeUpdate();
    +                    updateStatement.close();
    +
    +                } catch (SQLException e) {
    +                    s_logger.error("Failed to update cloud.account role_id for account id:" + accountId + " with exception: " + e.getMessage());
    +                    throw new CloudRuntimeException("Exception while updating cloud.account role_id", e);
    +                }
    +            }
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Exception while migrating existing account table's role_id column to a role based on account type", e);
    +        }
    +        s_logger.debug("Done migrating existing accounts to use one of default roles based on account type");
    +    }
    +
    +    private void setupRolesAndPermissionsForDynamicRBAC(final Connection conn) {
    +        try ( PreparedStatement selectStatement = conn.prepareStatement("SELECT * FROM `cloud`.`roles`");
    +              ResultSet resultSet = selectStatement.executeQuery()) {
    +            if (resultSet != null && resultSet.next()) {
    +                resultSet.close();
    +                if (s_logger.isDebugEnabled()) {
    +                    s_logger.debug("Found existing roles. Skipping migration of commands.properties to dynamic roles table.");
    +                }
    +                return;
    +            }
    +        } catch (SQLException e) {
    +            s_logger.error("Unable to find existing roles, if you need to add default roles please add them manually. Giving up!");
    --- End diff --
    
    Would it be useful to add the exception to log message?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59801226
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    --- End diff --
    
    Why not log this condition to ``WARN``?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60440106
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,170 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import org.apache.log4j.Logger;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    protected static final Logger LOGGER = Logger.getLogger(DynamicRoleBasedAPIAccessChecker.class);
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null || permissions.isEmpty() || Strings.isNullOrEmpty(commandName)) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            try {
    +                final Rule rule = new Rule(permission.getRule());
    +                if (rule.matches(commandName)) {
    +                    return true;
    +                }
    +            } catch (InvalidParameterValueException e) {
    +                LOGGER.warn("Invalid rule permission, please fix id=" + permission.getId() + " rule=" + permission.getRule());
    +                continue;
    +            }
    +        }
    +        return false;
    +    }
    +
    +    public boolean isDisabled() {
    +        return !roleService.isEnabled();
    +    }
    +
    +    @Override
    +    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
    +        if (isDisabled()) {
    +            return true;
    +        }
    +        Account account = accountService.getAccount(user.getAccountId());
    +        if (account == null) {
    +            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId() + "is null");
    +        }
    +
    +        final Role accountRole = roleService.findRole(account.getRoleId());
    +        if (accountRole == null || accountRole.getId() < 1L) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Allow all APIs for root admins
    +        if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) {
    +            return true;
    +        }
    +
    +        final List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(accountRole.getId());
    +
    +        // Check for allow rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.ALLOW, commandName)) {
    +            return true;
    +        }
    +
    +        // Check for deny rules
    +        if (checkPermission(rolePermissions, RolePermission.Permission.DENY, commandName)) {
    +            denyApiAccess(commandName);
    +        }
    +
    +        // Check annotations
    +        if (annotationRoleBasedApisMap.get(accountRole.getRoleType()) != null
    +                && annotationRoleBasedApisMap.get(accountRole.getRoleType()).contains(commandName)) {
    +            return true;
    +        }
    +
    +        denyApiAccess(commandName);
    +        return false;
    +    }
    +
    +    public void addApiToRoleBasedAnnotationsMap(final RoleType roleType, final String commandName) {
    +        if (roleType == null || Strings.isNullOrEmpty(commandName)) {
    +            return;
    +        }
    +        final Set<String> commands = annotationRoleBasedApisMap.get(roleType);
    +        if (commands != null && !commands.contains(commandName)) {
    +            commands.add(commandName);
    --- End diff --
    
    @koushik-das (3) from step 2, this methods populates an internal map that is a list of authorized apis for a given role types; we've only 4 role types used across cloudstack (and also in commands.properties aka the static checker) namely admin, resource admin, domain admin and user


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214281121
  
    @koushik-das can you share what you think are the advantages of static role-base api checker?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59858001
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    --- End diff --
    
    try with resource can have multiple resources, no need for explicit close


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-212129730
  
    @borisstoyanov awesome, thanks.  Are you able to push the test results to the PR?  I have built a tool (https://github.com/cloudops/upr) to hopefully simplify the pushing of test results back to the PR.  Since we don't have control of the github org (yet), you would need to use the `upr comment` subcommand.  


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59788517
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/DeleteRoleCmd.java ---
    @@ -0,0 +1,84 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.user.Account;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +import org.apache.cloudstack.api.response.SuccessResponse;
    +import org.apache.cloudstack.context.CallContext;
    +
    +@APICommand(name = DeleteRoleCmd.APINAME, description = "Deletes a role", responseObject = SuccessResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class DeleteRoleCmd extends BaseCmd {
    +    public static final String APINAME = "deleteRole";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.ID, type = BaseCmd.CommandType.UUID, required = true, entityType = RoleResponse.class, description = "ID of the role")
    +    private Long roleId;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public Long getRoleId() {
    +        return roleId;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() {
    +        if (getRoleId() == null || getRoleId() < 1L) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    +        Role role = roleService.findRole(getRoleId());
    +        if (role == null) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    --- End diff --
    
    Consider extracting this validation logic to a private method to make things more readable.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by borisstoyanov <gi...@git.apache.org>.
Github user borisstoyanov commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-214725108
  
    LGTM. :+1:


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-211320093
  
    Doc PR - https://github.com/apache/cloudstack-docs-admin/pull/37


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59800553
  
    --- Diff: api/test/org/apache/cloudstack/acl/RoleTypeTest.java ---
    @@ -0,0 +1,92 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.user.Account;
    +import org.junit.Assert;
    +import org.junit.Test;
    +import org.mockito.Mockito;
    +
    +import java.util.Arrays;
    +
    +public class RoleTypeTest {
    +
    +    @Test
    +    public void testRoleTypeFromString() {
    +        Assert.assertEquals(RoleType.fromString(null), null);
    +        Assert.assertEquals(RoleType.fromString(""), null);
    +        Assert.assertEquals(RoleType.fromString("admin"), null);
    +        Assert.assertEquals(RoleType.fromString("12345%^&*"), null);
    +        for (RoleType roleType : RoleType.values()) {
    +            Assert.assertEquals(RoleType.fromString(roleType.name()), roleType);
    +        }
    +    }
    +
    +    @Test
    +    public void testDefaultRoleMaskByValue() {
    +        Assert.assertEquals(RoleType.fromMask(1), RoleType.Admin);
    +        Assert.assertEquals(RoleType.fromMask(2), RoleType.ResourceAdmin);
    +        Assert.assertEquals(RoleType.fromMask(4), RoleType.DomainAdmin);
    +        Assert.assertEquals(RoleType.fromMask(8), RoleType.User);
    +        Assert.assertEquals(RoleType.fromMask(0), RoleType.Unknown);
    --- End diff --
    
    What happens if ``fromMask`` is passed unexpected integer values?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60880204
  
    --- Diff: test/integration/smoke/test_dynamicroles.py ---
    @@ -0,0 +1,474 @@
    +# 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.
    +
    +from marvin.cloudstackAPI import *
    +from marvin.cloudstackTestCase import cloudstackTestCase
    +from marvin.cloudstackException import CloudstackAPIException
    +from marvin.lib.base import Account, Role, RolePermission
    +from marvin.lib.utils import cleanup_resources
    +from nose.plugins.attrib import attr
    +
    +import random
    +import re
    +
    +
    +class TestData(object):
    +    """Test data object that is required to create resources
    +    """
    +    def __init__(self):
    +        self.testdata = {
    +            "account": {
    +                "email": "mtu@test.cloud",
    +                "firstname": "Marvin",
    +                "lastname": "TestUser",
    +                "username": "roletest",
    +                "password": "password",
    +            },
    +            "role": {
    +                "name": "MarvinFake Role ",
    +                "type": "User",
    +                "description": "Fake Role created by Marvin test"
    +            },
    +            "roleadmin": {
    +                "name": "MarvinFake Admin Role ",
    +                "type": "Admin",
    +                "description": "Fake Admin Role created by Marvin test"
    +            },
    +            "roledomainadmin": {
    +                "name": "MarvinFake DomainAdmin Role ",
    +                "type": "DomainAdmin",
    +                "description": "Fake Domain-Admin Role created by Marvin test"
    +            },
    +            "rolepermission": {
    +                "roleid": 1,
    +                "rule": "listVirtualMachines",
    +                "permission": "allow",
    +                "description": "Fake role permission created by Marvin test"
    +            },
    +            "apiConfig": {
    +                "listApis": "allow",
    +                "listAccounts": "allow",
    +                "listClusters": "deny",
    +                "*VM*": "allow",
    +                "*Host*": "deny"
    +            }
    +        }
    +
    +
    +class TestDynamicRoles(cloudstackTestCase):
    +    """Tests dynamic role and role permission management in CloudStack
    +    """
    +
    +    def setUp(self):
    +        self.apiclient = self.testClient.getApiClient()
    +        self.dbclient = self.testClient.getDbConnection()
    +        self.testdata = TestData().testdata
    +
    +        feature_enabled = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
    +        if not feature_enabled:
    +            self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
    +
    +        self.testdata["role"]["name"] += self.getRandomString()
    +        self.role = Role.create(
    +            self.apiclient,
    +            self.testdata["role"]
    +        )
    +
    +        self.testdata["rolepermission"]["roleid"] = self.role.id
    +        self.rolepermission = RolePermission.create(
    +            self.apiclient,
    +            self.testdata["rolepermission"]
    +        )
    +
    +        self.account = Account.create(
    +            self.apiclient,
    +            self.testdata["account"],
    +            roleid=self.role.id
    +        )
    +        self.cleanup = [
    +            self.account,
    +            self.rolepermission,
    +            self.role
    +        ]
    +
    +
    +    def tearDown(self):
    +        try:
    +           cleanup_resources(self.apiclient, self.cleanup)
    +        except Exception as e:
    +            self.debug("Warning! Exception in tearDown: %s" % e)
    +
    +
    +    def translateRoleToAccountType(self, role_type):
    +        if role_type == "User":
    +            return 0
    +        elif role_type == "Admin":
    +            return 1
    +        elif role_type == "DomainAdmin":
    +            return 2
    +        elif role_type == "ResourceAdmin":
    +            return 3
    +        return -1
    +
    +
    +    def getUserApiClient(self, username, domain='ROOT', role_type='User'):
    +        self.user_apiclient = self.testClient.getUserApiClient(UserName=username, DomainName='ROOT', type=self.translateRoleToAccountType(role_type))
    +        return self.user_apiclient
    +
    +
    +    def getRandomString(self):
    +        return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10))
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_list(self):
    +        """
    +            Tests that default four roles exist
    +        """
    +        roleTypes = {1: "Admin", 2: "ResourceAdmin", 3: "DomainAdmin", 4: "User"}
    +        for idx in range(1,5):
    +            list_roles = Role.list(self.apiclient, id=idx)
    +            self.assertEqual(
    +                isinstance(list_roles, list),
    +                True,
    +                "List Roles response was not a valid list"
    +            )
    +            self.assertEqual(
    +                len(list_roles),
    +                1,
    +                "List Roles response size was not 1"
    +            )
    +            self.assertEqual(
    +                list_roles[0].type,
    +                roleTypes[idx],
    +                msg="Default role type differs from expectation"
    +            )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_create(self):
    +        """
    +            Tests normal lifecycle operations for roles
    +        """
    +        # Reuse self.role created in setUp()
    +        try:
    +            role = Role.create(
    +                self.apiclient,
    +                self.testdata["role"]
    +            )
    +            self.fail("An exception was expected when creating duplicate roles")
    +        except CloudstackAPIException: pass
    +
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            isinstance(list_roles, list),
    +            True,
    +            "List Roles response was not a valid list"
    +        )
    +        self.assertEqual(
    +            len(list_roles),
    +            1,
    +            "List Roles response size was not 1"
    +        )
    +        self.assertEqual(
    +            list_roles[0].name,
    +            self.testdata["role"]["name"],
    +            msg="Role name does not match the test data"
    +        )
    +        self.assertEqual(
    +            list_roles[0].type,
    +            self.testdata["role"]["type"],
    +            msg="Role type does not match the test data"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +        self.assertEqual(
    +            update_role.type,
    +            'Admin',
    +            msg="Role type does not match updated role type"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_update_role_inuse(self):
    +        """
    +            Tests role update when role is in use by an account
    +        """
    +        new_role_name = "MarvinFakeRoleNewName-" + self.getRandomString()
    +        try:
    +            self.role.update(self.apiclient, name=new_role_name, type='Admin')
    +            self.fail("Updation of role type is not allowed when role is in use")
    +        except CloudstackAPIException: pass
    +
    +        self.role.update(self.apiclient, name=new_role_name)
    +        update_role = Role.list(self.apiclient, id=self.role.id)[0]
    +        self.assertEqual(
    +            update_role.name,
    +            new_role_name,
    +            msg="Role name does not match updated role name"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_lifecycle_delete(self):
    +        """
    +            Tests role update
    +        """
    +        self.account.delete(self.apiclient)
    +        self.role.delete(self.apiclient)
    +        list_roles = Role.list(self.apiclient, id=self.role.id)
    +        self.assertEqual(
    +            list_roles,
    +            None,
    +            "List Roles response should be empty"
    +        )
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_role_inuse_deletion(self):
    +        """
    +            Test to ensure role in use cannot be deleted
    +        """
    +        try:
    +            self.role.delete(self.apiclient)
    +            self.fail("Role with any account should not be allowed to be deleted")
    +        except CloudstackAPIException: pass
    +
    +
    +    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
    +    def test_default_role_deletion(self):
    +        """
    +            Test to ensure 4 default roles cannot be deleted
    +        """
    +        for idx in range(1,5):
    +            cmd = deleteRole.deleteRoleCmd()
    +            cmd.id = idx
    +            try:
    +                self.apiclient.deleteRole(cmd)
    +                self.fail("Default role got deleted with id: " + idx)
    +            except CloudstackAPIException: pass
    --- End diff --
    
    We can do that but since we've catching any ACS server api exception caught by CloudStackAPIException, we know it's ACS specific API exception and not any HTTP exception.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59800096
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java ---
    @@ -117,6 +123,12 @@ public long getEntityOwnerId() {
     
         @Override
         public void execute() {
    +        if (Strings.isNullOrEmpty(getCfgName())) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty configuration name provided");
    +        }
    +        if (getCfgName().equalsIgnoreCase(RoleService.EnableDynamicApiChecker.key())) {
    +            throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "Restricted configuration update not allowed");
    --- End diff --
    
    Method Not Allowed (405) indicates the caller attempted to use an HTTP method (i.e. GET, PUT, POST, DELETE) that is not allowed for the resource.  In this case, the method is allowed, but the parameters do not all the method to be performed.  Seems like ``PARAM_ERROR`` would be more appropriate.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by mlsorensen <gi...@git.apache.org>.
Github user mlsorensen commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-215147096
  
    @koushik-das -- I'd say yes, that testing is good enough. If a test can determine that the default roles allow the expected access to the APIs then we're good.
    
    There was some discussion of commands.properties deprecation in 2013. In particular, plugin developers had issues with the way it works. People should really have been using the annotations by now, but I believe it was not well communicated and the fact that commands.properties is still around has confused new developers. From the original discussion, the only reason it was left in place was in case users wanted to disable APIs entirely by setting the bitmask of an API to 0. Do we have that functionality covered now?
    
    http://mail-archives.apache.org/mod_mbox/cloudstack-dev/201310.mbox/%3C2B439486-9C4B-41B7-A698-B7ADE234B370@netapp.com%3E
    
    https://github.com/apache/cloudstack/commit/9f7b4884a7a0bf0b15d94777cff449fde4664af2
    
    Note also that existing installations are not being affected, they have to choose to use the new mechanism. In theory, I'm fine with some sort of staged transition where the dynamic checking is opt-in for a release, but I worry that it will perpetuate the issue we've seen since 2013, and puts a burden on the community to go back and clean up at some later date. For these reasons, I think if it's functionally identical on new installs and doesn't affect existing installs use of commands.properties, we should just cut over.



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by borisstoyanov <gi...@git.apache.org>.
Github user borisstoyanov commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-212381873
  
    Hey guys, Just a FYI: I'll be addressing the testing of this PR at the end of next week since I'm occupied with other tasks at this time.  


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60732469
  
    --- Diff: engine/schema/src/com/cloud/upgrade/dao/Upgrade481to490.java ---
    @@ -53,6 +62,139 @@ public boolean supportsRollingUpgrade() {
     
         @Override
         public void performDataMigration(Connection conn) {
    +        setupRolesAndPermissionsForDynamicRBAC(conn);
    +    }
    +
    +    private void createDefaultRole(final Connection conn, final Long id, final String name, final RoleType roleType) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`roles` (`id`, `uuid`, `name`, `role_type`, `description`) values (%d, UUID(), '%s', '%s', 'Default %s role');",
    +                id, name, roleType.name(), roleType.name().toLowerCase());
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql) ) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException e) {
    +            throw new CloudRuntimeException("Unable to create default role with id: " + id + " name: " + name, e);
    +        }
    +    }
    +
    +    private void createRoleMapping(final Connection conn, final Long roleId, final String apiName) {
    +        final String insertSql = String.format("INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`) values (UUID(), %d, '%s', 'ALLOW') ON DUPLICATE KEY UPDATE rule=rule;",
    +                roleId, apiName);
    +        try ( PreparedStatement updatePstmt = conn.prepareStatement(insertSql)) {
    +            updatePstmt.executeUpdate();
    +        } catch (SQLException ignored) {
    +            s_logger.debug("Unable to insert mapping for role id:" + roleId + " apiName: " + apiName);
    +        }
    +    }
    +
    +    private void addRoleColumnAndMigrateAccountTable(final Connection conn, final RoleType[] roleTypes) {
    +        final String alterTableSql = "ALTER TABLE `cloud`.`account` ADD COLUMN `role_id` bigint(20) unsigned COMMENT 'role id for this account' AFTER `type`, " +
    +                "ADD KEY `fk_account__role_id` (`role_id`), " +
    +                "ADD CONSTRAINT `fk_account__role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`);";
    +        try (PreparedStatement pstmt = conn.prepareStatement(alterTableSql)) {
    +            pstmt.executeUpdate();
    +            s_logger.info("Altered cloud.account table and added column role_id");
    +        } catch (SQLException e) {
    +            if (e.getMessage().contains("role_id")) {
    +                s_logger.warn("cloud.account table already has the role_id column, skipping altering table and migration of accounts");
    +                return;
    +            } else {
    +                throw new CloudRuntimeException("Unable to create column quota_calculated in table cloud_usage.cloud_usage", e);
    +            }
    +        }
    +        migrateAccountsToDefaultRoles(conn, roleTypes);
    +    }
    +
    +    private void migrateAccountsToDefaultRoles(final Connection conn, final RoleType[] roleTypes) {
    +        try (PreparedStatement selectStatement = conn.prepareStatement("SELECT `id`, `type` FROM `cloud`.`account`;");
    +             ResultSet selectResultSet = selectStatement.executeQuery()) {
    --- End diff --
    
    @bhaisaab ``selectResultSet`` needs to be added to the enclosing try with resources block.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60369577
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            final String rule = permission.getRule();
    +            if (rule.contains("*")) {
    +                if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                    return true;
    +                }
    +            } else {
    +                if (commandName.equals(rule)) {
    --- End diff --
    
    @koushik-das just like commands.properties declarations the API name much match provided rule. Though, I see your point here -- let me think if this can have any side-effect or some use-case where this can fail; otherwise I'll fix this.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r60384791
  
    --- Diff: plugins/acl/dynamic-role-based/src/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java ---
    @@ -0,0 +1,166 @@
    +// 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.cloudstack.acl;
    +
    +import com.cloud.exception.PermissionDeniedException;
    +import com.cloud.user.Account;
    +import com.cloud.user.AccountService;
    +import com.cloud.user.User;
    +import com.cloud.utils.component.AdapterBase;
    +import com.cloud.utils.component.PluggableService;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.api.APICommand;
    +
    +import javax.ejb.Local;
    +import javax.inject.Inject;
    +import javax.naming.ConfigurationException;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +
    +@Local(value = APIChecker.class)
    +public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {
    +
    +    @Inject
    +    private AccountService accountService;
    +    @Inject
    +    private RoleService roleService;
    +
    +    private List<PluggableService> services;
    +    private Map<RoleType, Set<String>> annotationRoleBasedApisMap = new HashMap<>();
    +
    +    protected DynamicRoleBasedAPIAccessChecker() {
    +        super();
    +        for (RoleType roleType : RoleType.values()) {
    +            annotationRoleBasedApisMap.put(roleType, new HashSet<String>());
    +        }
    +    }
    +
    +    private void denyApiAccess(final String commandName) throws PermissionDeniedException {
    +        throw new PermissionDeniedException("The API does not exist or is blacklisted for the account's role. " +
    +                "The account with is not allowed to request the api: " + commandName);
    +    }
    +
    +    private boolean checkPermission(final List <? extends RolePermission> permissions, final RolePermission.Permission permissionToCheck, final String commandName) {
    +        if (permissions == null) {
    +            return false;
    +        }
    +        for (final RolePermission permission : permissions) {
    +            if (permission.getPermission() != permissionToCheck) {
    +                continue;
    +            }
    +            final String rule = permission.getRule();
    +            if (rule.contains("*")) {
    +                if (commandName.matches(rule.replace("*", "\\w*"))) {
    +                    return true;
    +                }
    +            } else {
    +                if (commandName.equals(rule)) {
    --- End diff --
    
    @koushik-das I spent some time thinking and I think it should be okay to do a ignore-case equality check. Fixed, it does equalsIgnoreCase now.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59523317
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/CreateRoleCmd.java ---
    @@ -0,0 +1,104 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.user.Account;
    +import com.google.common.base.Strings;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +import org.apache.cloudstack.context.CallContext;
    +
    +@APICommand(name = CreateRoleCmd.APINAME, description = "Creates a role", responseObject = RoleResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class CreateRoleCmd extends BaseCmd {
    +    public static final String APINAME = "createRole";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "creates a role with this unique name")
    +    private String roleName;
    +
    +    @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "The type of the role, valid options are: Admin, ResourceAdmin, DomainAdmin, User")
    +    private String roleType;
    +
    +    @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the role")
    +    private String roleDescription;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public String getRoleName() {
    +        return roleName;
    +    }
    +
    +    public RoleType getRoleType() {
    +        return RoleType.fromString(roleType);
    +    }
    +
    +    public String getRoleDescription() {
    +        return roleDescription;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() {
    +        if (Strings.isNullOrEmpty(getRoleName())) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty role name provided");
    +        }
    +        if (getRoleType() == null || getRoleType() == RoleType.Unknown) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role type provided");
    +        }
    +        CallContext.current().setEventDetails("Role: " + getRoleName() + ", type:" + getRoleType() + ", description: " + getRoleDescription());
    +        Role role = roleService.createRole(getRoleName(), getRoleType(), getRoleDescription());
    +        if (role == null) {
    +            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create role");
    +        }
    +        RoleResponse response = new RoleResponse();
    +        response.setId(role.getUuid());
    +        response.setRoleName(role.getName());
    +        response.setRoleType(role.getRoleType());
    +        response.setResponseName(getCommandName());
    +        response.setObjectName("role");
    +        setResponseObject(response);
    +    }
    --- End diff --
    
    I can do that, but won't make any difference. By keeping it in the execute() we can keep the code that makes response in the same place. This is a common pattern in several api Cmd classes.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59773313
  
    --- Diff: api/src/org/apache/cloudstack/acl/RoleType.java ---
    @@ -16,18 +16,90 @@
     // under the License.
     package org.apache.cloudstack.acl;
     
    +import com.cloud.user.Account;
    +import com.google.common.base.Enums;
    +import com.google.common.base.Strings;
    +
     // Enum for default roles in CloudStack
     public enum RoleType {
    -    Admin(1), ResourceAdmin(2), DomainAdmin(4), User(8), Unknown(0);
    +    Admin(1L, Account.ACCOUNT_TYPE_ADMIN, 1),
    +    ResourceAdmin(2L, Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN, 2),
    +    DomainAdmin(3L, Account.ACCOUNT_TYPE_DOMAIN_ADMIN, 4),
    +    User(4L, Account.ACCOUNT_TYPE_NORMAL, 8),
    +    Unknown(-1L, (short) -1, 0);
     
    +    private long id;
    +    private short accountType;
         private int mask;
     
    -    private RoleType(int mask) {
    +    RoleType(final long id, final short accountType, final int mask) {
    +        this.id = id;
    +        this.accountType = accountType;
             this.mask = mask;
         }
     
    -    public int getValue() {
    +    public long getId() {
    +        return id;
    +    }
    +
    +    public short getAccountType() {
    +        return accountType;
    +    }
    +
    +    public int getMask() {
             return mask;
         }
    -}
     
    +    public static RoleType fromString(final String name) {
    +        if (!Strings.isNullOrEmpty(name)
    +                && Enums.getIfPresent(RoleType.class, name).isPresent()) {
    +            return RoleType.valueOf(name);
    +        }
    +        return null;
    +    }
    +
    +    public static RoleType fromMask(int mask) {
    +        for (RoleType roleType : RoleType.values()) {
    +            if (roleType.getMask() == mask) {
    +                return roleType;
    +            }
    +        }
    +        return Unknown;
    +    }
    +
    +    public static RoleType getByAccountType(final short accountType) {
    +        RoleType roleType = RoleType.Unknown;
    +        switch (accountType) {
    +            case Account.ACCOUNT_TYPE_ADMIN:
    +                roleType = RoleType.Admin;
    +                break;
    +            case Account.ACCOUNT_TYPE_DOMAIN_ADMIN:
    +                roleType = RoleType.DomainAdmin;
    +                break;
    +            case Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN:
    +                roleType = RoleType.ResourceAdmin;
    +                break;
    +            case Account.ACCOUNT_TYPE_NORMAL:
    +                roleType = RoleType.User;
    +                break;
    +        }
    +        return roleType;
    +    }
    +
    +    public static Long getRoleByAccountType(final Long roleId, final Short accountType) {
    --- End diff --
    
    This is for doing backward compatiblity in create APIs when roleId passed is null, but an account type is passed. This does the translation using above method.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by bhaisaab <gi...@git.apache.org>.
Github user bhaisaab commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59857842
  
    --- Diff: api/test/org/apache/cloudstack/acl/RuleTest.java ---
    @@ -0,0 +1,39 @@
    +package org.apache.cloudstack.acl;
    +
    +import com.cloud.exception.InvalidParameterValueException;
    +import org.junit.Assert;
    +import org.junit.Test;
    +
    +import java.util.Arrays;
    +
    +public class RuleTest {
    +
    +    @Test
    +    public void testToString() throws Exception {
    +        Rule rule = new Rule("someString");
    +        Assert.assertEquals(rule.toString(), "someString");
    +    }
    +
    +    @Test
    +    public void testValidateRuleWithValidData() throws Exception {
    +        for (String rule : Arrays.asList("a", "1", "someApi", "someApi321", "123SomeApi",
    +                "prefix*", "*middle*", "*Suffix",
    +                "*", "**", "f***", "m0nk3yMa**g1c*")) {
    +            Assert.assertTrue(Rule.validate(rule));
    +            Assert.assertEquals(new Rule(rule).toString(), rule);
    +        }
    +    }
    +
    +    @Test
    +    public void testValidateRuleWithInvalidData() throws Exception {
    +        for (String rule : Arrays.asList(null, "", " ", "  ", "\n", "\t", "\r", "\"", "\'",
    +                "^someApi$", "^someApi", "some$", "some-Api;", "some,Api",
    +                "^", "$", "^$", ".*", "\\w+", "r**l3rd0@Kr3", "j@s1n|+|0ȷ",
    +                "[a-z0-9-]+", "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$")) {
    +            try {
    +                new Rule(rule);
    +                Assert.fail("Invalid rule, exception was expected");
    +            } catch (InvalidParameterValueException ignored) {}
    +        }
    --- End diff --
    
    It's in a for loop, so won't do


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by DaanHoogland <gi...@git.apache.org>.
Github user DaanHoogland commented on a diff in the pull request:

    https://github.com/apache/cloudstack/pull/1489#discussion_r59513157
  
    --- Diff: api/src/org/apache/cloudstack/api/command/admin/acl/ListRolePermissionsCmd.java ---
    @@ -0,0 +1,104 @@
    +// 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.cloudstack.api.command.admin.acl;
    +
    +import com.cloud.exception.InsufficientCapacityException;
    +import com.cloud.exception.ResourceUnavailableException;
    +import com.cloud.user.Account;
    +import org.apache.cloudstack.acl.Role;
    +import org.apache.cloudstack.acl.RolePermission;
    +import org.apache.cloudstack.acl.RoleType;
    +import org.apache.cloudstack.api.APICommand;
    +import org.apache.cloudstack.api.ApiConstants;
    +import org.apache.cloudstack.api.ApiErrorCode;
    +import org.apache.cloudstack.api.BaseCmd;
    +import org.apache.cloudstack.api.Parameter;
    +import org.apache.cloudstack.api.ServerApiException;
    +import org.apache.cloudstack.api.response.ListResponse;
    +import org.apache.cloudstack.api.response.RolePermissionResponse;
    +import org.apache.cloudstack.api.response.RoleResponse;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +
    +@APICommand(name = ListRolePermissionsCmd.APINAME, description = "Lists role permissions", responseObject = RolePermissionResponse.class,
    +        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
    +        since = "4.9.0",
    +        authorized = {RoleType.Admin})
    +public class ListRolePermissionsCmd extends BaseCmd {
    +    public static final String APINAME = "listRolePermissions";
    +
    +    /////////////////////////////////////////////////////
    +    //////////////// API parameters /////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Parameter(name = ApiConstants.ROLE_ID, type = CommandType.UUID, entityType = RoleResponse.class, description = "ID of the role")
    +    private Long roleId;
    +
    +    /////////////////////////////////////////////////////
    +    /////////////////// Accessors ///////////////////////
    +    /////////////////////////////////////////////////////
    +
    +    public Long getRoleId() {
    +        return roleId;
    +    }
    +
    +    /////////////////////////////////////////////////////
    +    /////////////// API Implementation///////////////////
    +    /////////////////////////////////////////////////////
    +
    +    @Override
    +    public String getCommandName() {
    +        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
    +    }
    +
    +    @Override
    +    public long getEntityOwnerId() {
    +        return Account.ACCOUNT_ID_SYSTEM;
    +    }
    +
    +    @Override
    +    public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException {
    +        if (getRoleId() != null && getRoleId() < 1L) {
    +            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided");
    +        }
    +        List<RolePermission> rolePermissions = roleService.findAllPermissionsBy(getRoleId());
    +
    +        ListResponse<RolePermissionResponse> response = new ListResponse<>();
    +        List<RolePermissionResponse> rolePermissionResponses = new ArrayList<>();
    +        for (RolePermission rolePermission : rolePermissions) {
    +            Role role = roleService.findRole(rolePermission.getRoleId());
    +            if (role == null) {
    +                continue;
    +            }
    +            RolePermissionResponse rolePermissionResponse = new RolePermissionResponse();
    +            rolePermissionResponse.setRoleId(role.getUuid());
    +            rolePermissionResponse.setRoleName(role.getName());
    +            rolePermissionResponse.setId(rolePermission.getUuid());
    +            rolePermissionResponse.setRule(rolePermission.getRule());
    +            rolePermissionResponse.setRulePermission(rolePermission.getPermission());
    +            rolePermissionResponse.setDescription(rolePermission.getDescription());
    +            rolePermissionResponse.setObjectName("rolepermission");
    +            rolePermissionResponses.add(rolePermissionResponse);
    +        }
    +        response.setResponses(rolePermissionResponses);
    +        response.setResponseName(getCommandName());
    +        setResponseObject(response);
    +    }
    --- End diff --
    
    same as creat commends: factor out?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by anshul1886 <gi...@git.apache.org>.
Github user anshul1886 commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-220243535
  
    @rhtyd It looks fine now. It was present in some tomcat folder.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit closed the pull request at:

    https://github.com/apache/cloudstack/pull/1489


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by swill <gi...@git.apache.org>.
Github user swill commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-218356331
  
    Sorry to do this to you @rhtyd, but I just merged like 10 PRs and we now have merge conflicts.  Can you resolve the merge conflicts and re-push.  When you resolve the merge conflicts, would you mind not squashing the fixes to the merge conflicts so we can more easily review the changes required to resolve the merge conflicts.  This will help me know if I need to re-run CI before I merge this.  Thanks...


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by rhtyd <gi...@git.apache.org>.
Github user rhtyd commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-217413190
  
    @swill this PR is ready for CI test run and merge, thanks


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] cloudstack pull request: CLOUDSTACK-8562: Dynamic Role-Based API C...

Posted by jburwell <gi...@git.apache.org>.
Github user jburwell commented on the pull request:

    https://github.com/apache/cloudstack/pull/1489#issuecomment-217450750
  
    @rhtyd @swill LGTM based on code review


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---