// 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.iam;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;
import javax.naming.ConfigurationException;

import org.apache.log4j.Logger;

import org.apache.cloudstack.acl.APIChecker;
import org.apache.cloudstack.acl.PermissionScope;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.iam.api.IAMPolicy;
import org.apache.cloudstack.iam.api.IAMPolicyPermission;
import org.apache.cloudstack.iam.api.IAMPolicyPermission.Permission;
import org.apache.cloudstack.iam.api.IAMService;

import com.cloud.api.ApiServerService;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.User;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.component.AdapterBase;
import com.cloud.utils.component.PluggableService;
import com.cloud.utils.exception.CloudRuntimeException;

//This is the Role Based API access checker that grab's the  account's roles
//based on the set of roles, access is granted if any of the role has access to the api
public class RoleBasedAPIAccessChecker extends AdapterBase implements APIChecker {

    protected static final Logger s_logger = Logger.getLogger(RoleBasedAPIAccessChecker.class);

    @Inject
    AccountService _accountService;
    @Inject
    ApiServerService _apiServer;
    @Inject
    IAMService _iamSrv;
    @Inject
    VMTemplateDao _templateDao;

    Set<String> commandsPropertiesOverrides = new HashSet<String>();
    Map<RoleType, Set<String>> commandsPropertiesRoleBasedApisMap = new HashMap<RoleType, Set<String>>();

    List<PluggableService> _services;

    protected RoleBasedAPIAccessChecker() {
        super();
        for (RoleType roleType : RoleType.values()) {
            commandsPropertiesRoleBasedApisMap.put(roleType, new HashSet<String>());
        }
     }

    @Override
    public boolean checkAccess(User user, String commandName) throws PermissionDeniedException {
        Account account = _accountService.getAccount(user.getAccountId());
        if (account == null) {
            throw new PermissionDeniedException("The account id=" + user.getAccountId() + "for user id=" + user.getId()
                    + "is null");
        }

        List<IAMPolicy> policies = _iamSrv.listIAMPolicies(account.getAccountId());

        boolean isAllowed = _iamSrv.isActionAllowedForPolicies(commandName, policies);
        if (!isAllowed) {
            throw new PermissionDeniedException("The API does not exist or is blacklisted. api: " + commandName);
        }
        return isAllowed;
     }

    @Override
    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
        super.configure(name, params);

        processMapping(PropertiesUtil.processConfigFile(new String[] { "commands.properties" }));
        return true;
     }

    @Override
    public boolean start() {

        // drop all default policy api permissions - we reload them every time
        // to include any changes done to the @APICommand or
        // commands.properties.

        for (RoleType role : RoleType.values()) {
            Long policyId = getDefaultPolicyId(role);
            if (policyId != null) {
                _iamSrv.resetIAMPolicy(policyId);
            }
         }

        // add the system-domain capability

        _iamSrv.addIAMPermissionToIAMPolicy(new Long(Account.ACCOUNT_TYPE_ADMIN + 1), null, null, null,
                "SystemCapability", null, Permission.Allow, false);
        _iamSrv.addIAMPermissionToIAMPolicy(new Long(Account.ACCOUNT_TYPE_DOMAIN_ADMIN + 1), null, null, null,
                "DomainCapability", null, Permission.Allow, false);
        _iamSrv.addIAMPermissionToIAMPolicy(new Long(Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN + 1), null, null, null,
                "DomainResourceCapability", null, Permission.Allow, false);

        // add permissions for public templates
        List<VMTemplateVO> pTmplts = _templateDao.listByPublic();
        for (VMTemplateVO tmpl : pTmplts){
            _iamSrv.addIAMPermissionToIAMPolicy(new Long(Account.ACCOUNT_TYPE_ADMIN + 1), VirtualMachineTemplate.class.getSimpleName(),
                    PermissionScope.RESOURCE.toString(), tmpl.getId(), "listTemplates", AccessType.UseEntry.toString(), Permission.Allow, false);
            _iamSrv.addIAMPermissionToIAMPolicy(new Long(Account.ACCOUNT_TYPE_DOMAIN_ADMIN + 1), VirtualMachineTemplate.class.getSimpleName(),
                    PermissionScope.RESOURCE.toString(), tmpl.getId(), "listTemplates", AccessType.UseEntry.toString(), Permission.Allow, false);
            _iamSrv.addIAMPermissionToIAMPolicy(new Long(Account.ACCOUNT_TYPE_NORMAL + 1), VirtualMachineTemplate.class.getSimpleName(),
                    PermissionScope.RESOURCE.toString(), tmpl.getId(), "listTemplates", AccessType.UseEntry.toString(), Permission.Allow, false);
        }

        for (PluggableService service : _services) {
            for (Class<?> cmdClass : service.getCommands()) {
                APICommand command = cmdClass.getAnnotation(APICommand.class);
                if (!commandsPropertiesOverrides.contains(command.name())) {
                    for (RoleType role : command.authorized()) {
                        addDefaultAclPolicyPermission(command.name(), cmdClass, role);
                    }
                 }
             }
         }

        // read commands.properties and load api acl permissions -
        // commands.properties overrides any @APICommand authorization

        for (String apiName : commandsPropertiesOverrides) {
            Class<?> cmdClass = _apiServer.getCmdClass(apiName);
            for (RoleType role : RoleType.values()) {
                if (commandsPropertiesRoleBasedApisMap.get(role).contains(apiName)) {
                    // insert permission for this role for this api
                    addDefaultAclPolicyPermission(apiName, cmdClass, role);
                }
             }
         }

        return super.start();
     }

    private Long getDefaultPolicyId(RoleType role) {
        Long policyId = null;
        switch (role) {
        case User:
            policyId = new Long(Account.ACCOUNT_TYPE_NORMAL + 1);
            break;

        case Admin:
            policyId = new Long(Account.ACCOUNT_TYPE_ADMIN + 1);
            break;

        case DomainAdmin:
            policyId = new Long(Account.ACCOUNT_TYPE_DOMAIN_ADMIN + 1);
            break;

        case ResourceAdmin:
            policyId = new Long(Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN + 1);
            break;
        }

        return policyId;
    }

    private void processMapping(Map<String, String> configMap) {
        for (Map.Entry<String, String> entry : configMap.entrySet()) {
            String apiName = entry.getKey();
            String roleMask = entry.getValue();
            commandsPropertiesOverrides.add(apiName);
            try {
                short cmdPermissions = Short.parseShort(roleMask);
                for (RoleType roleType : RoleType.values()) {
                    if ((cmdPermissions & roleType.getMask()) != 0)
                        commandsPropertiesRoleBasedApisMap.get(roleType).add(apiName);
                }
            } catch (NumberFormatException nfe) {
                s_logger.info("Malformed key=value pair for entry: " + entry.toString());
             }
         }
     }

    public List<PluggableService> getServices() {
        return _services;
     }

    @Inject
    public void setServices(List<PluggableService> services) {
        _services = services;
     }

    private void addDefaultAclPolicyPermission(String apiName, Class<?> cmdClass, RoleType role) {
        AccessType accessType = null;
        Class<?>[] entityTypes = null;

        PermissionScope permissionScope = PermissionScope.ACCOUNT;
        Long policyId = getDefaultPolicyId(role);
        switch (role) {
        case User:
            permissionScope = PermissionScope.ACCOUNT;
            break;

        case Admin:
            permissionScope = PermissionScope.ALL;
            break;

        case DomainAdmin:
            permissionScope = PermissionScope.DOMAIN;
            break;

        case ResourceAdmin:
            permissionScope = PermissionScope.DOMAIN;
            break;
         }

        boolean addAccountScopedUseEntry = false;

        if (cmdClass != null) {
            BaseCmd cmdObj;
            try {
                cmdObj = (BaseCmd) cmdClass.newInstance();
                if (cmdObj instanceof BaseListCmd) {
                    if (permissionScope == PermissionScope.ACCOUNT) {
                        accessType = AccessType.UseEntry;
                    } else {
                        accessType = AccessType.ListEntry;
                        addAccountScopedUseEntry = true;
                    }
                } else {
                    accessType = AccessType.OperateEntry;
                }
            } catch (Exception e) {
                throw new CloudRuntimeException(String.format(
                        "%s is claimed as an API command, but it cannot be instantiated", cmdClass.getName()));
            }

            APICommand at = cmdClass.getAnnotation(APICommand.class);
            entityTypes = at.entityType();
        }

        if (entityTypes == null || entityTypes.length == 0) {
            _iamSrv.addIAMPermissionToIAMPolicy(policyId, null, permissionScope.toString(), new Long(IAMPolicyPermission.PERMISSION_SCOPE_ID_CURRENT_CALLER),
                    apiName, (accessType == null) ? null : accessType.toString(), Permission.Allow, false);
            if (addAccountScopedUseEntry) {
                _iamSrv.addIAMPermissionToIAMPolicy(policyId, null, PermissionScope.ACCOUNT.toString(), new Long(
                        IAMPolicyPermission.PERMISSION_SCOPE_ID_CURRENT_CALLER), apiName, AccessType.UseEntry.toString(), Permission.Allow, false);
            }
        } else {
            for (Class<?> entityType : entityTypes) {
                _iamSrv.addIAMPermissionToIAMPolicy(policyId, entityType.getSimpleName(), permissionScope.toString(), new Long(
                        IAMPolicyPermission.PERMISSION_SCOPE_ID_CURRENT_CALLER),
                        apiName, (accessType == null) ? null : accessType.toString(), Permission.Allow, false);
                if (addAccountScopedUseEntry) {
                    _iamSrv.addIAMPermissionToIAMPolicy(policyId, entityType.getSimpleName(), PermissionScope.ACCOUNT.toString(), new Long(
                            IAMPolicyPermission.PERMISSION_SCOPE_ID_CURRENT_CALLER), apiName, AccessType.UseEntry.toString(), Permission.Allow, false);
                }
            }
         }

     }

}
