Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3

Table of Contents
indent20px
styledisc

Background

Currently CloudStack provides very limited IAM services and there are several drawbacks within those services:

  • Offers few roles out of the box (user and admin) with prebaked access control for these roles. There is no way to create additional roles with customized permissions.
  • Some resources have access control baked into them. E.g., shared networks, projects etc.
  • We have to create special dedicate API to grant permissions.

Goal for this feature would be to address these limitations and offer true IAM services in a phased manner.

JIRA ticket

Ticket to track the feature implementation: https://issues.apache.org/jira/browse/CLOUDSTACK-5920

Note: IAM feature cannot be put in to current 4.5 codebase due to API gap and limitations found and documented here: https://cwiki.apache.org/confluence/display/CLOUDSTACK/API+changes

Architecture and Design description

IAM Taxonomy

Gliffy Diagram
sizeL
nameiamTaxonomy

Group

Group contains a number of CloudStack accounts. Customers should be able to Create, Edit, List and Delete Groups. Editing includes adding or removing accounts to or from a group. For backwards compatibility, out of box, CloudStack will provide 3 default groups:

  • Root Admin Group
  • Domain Admin Group
  • End User Group

Account

Account is just our current CloudStack Account, all the permission controls are done at Account level. We can assign an Account to more than one Group.

User

CloudStack user just contains login credentials, and this is not the level that we are performing permission control.

Policy

Policy is a set of permission. Customer should be able to attach several policies to a Group to define the permission for that group. By default, we have the following 3 types of policy templates:

  1. Root Admin Policy: have permissions to all resources in the CloudStack for allowed APIs.
  2. Domain Admin Policy: have permissions to all resources under the belonging domain for allowed APIs.
  3. Resource Owner Policy: have permissions to all owned resources for allowed APIs.

Other than that, customer should be able to define customized policies by grant or deny permission to customize permissions for the group. So far, for cross-account permission grant, we are currently supporting the following 3 types of granting/denying:

  • Grant by Domain and Entity Type: grant permissions to all resources of the given entity type under the given domain.
  • Grant by Account and Entity Type: grant permissions to all resources of the given entity type under the given account.
  • Grant by individual resource: grant permission to an individual resource.

Entity Type

Here are list of entity types supported by IAM model:

  • VirtualMachine
  • Volume
  • ResourceTag
  • Account
  • AffinityGroup
  • AutoScalePolicy
  • AutoScaleVmProfile
  • AutoScaleVmGroup
  • Condition
  • Vpc
  • VpcGateway
  • VMSnapshot
  • VirtualMachineTemplate
  • StaticRoute
  • Snapshot
  • Site2SiteVpnGateway
  • Site2SiteCustomerGateway
  • Site2SiteVpnConnection
  • SecurityGroup
  • RemoteAccessVpn
  • ProjectInvitation
  • Network
  • IPAddress
  • InstanceGroup
  • GlobalLoadBalancerRule
  • FirewallRule
  • PortForwardingRule
  • Event

Permission

A policy consists of set of Permissions. A Permission is a way of defining access control.
Using Permission, customer defines what actions are allowed or denied, on what resources, under which account or domain.

A single permission definition consists of:

  • Action (API Name)
  • Allow / Deny
  • Scope (Account | Domain | Resource)
  • Scope Id (Id of the above defined scope)
  • Resource Type

Response View (Column Filter)

Currently CloudStack supports two different views: Full View for root admin user, Restricted View for domain admin and end user. With IAM added, we still need to support this for newly added account groups. Analogy to row filter controlled by Permission, this is like column filter. For this release, we are not supporting dynamic column filter at API level, instead, out-of-box, we will provide two static response views: Full View and Restricted View. When user is granting each API action to a group, they can specify what kind of response view for this API. For this release, we are supporting static response view choice for the following APIs:

  • listAccounts
  • listZones
  • listTemplates
  • createTemplate
  • copyTemplate
  • registerTemplate
  • listTemplatePermissions
  • registerIso
  • copyIso
  • listIsos
  • attachIso
  • detachIso
  • listIsoPermissions
  • updateVMAffinityGroup
  • addNicToVirtualMachine
  • removeNicFromVirtualMachine
  • updateDefaultNicForVirtualMachine
  • listLoadBalancerRuleInstances
  • deployVirtualMachine
  • destroyVirtualMachine
  • rebootVirtualMachine
  • resetPasswordForVirtualMachine
  • resetSSHKeyForVirtualMachine
  • restoreVirtualMachine
  • scaleVirtualMachine
  • startVirtualMachine
  • stopVirtualMachine
  • updateVirtualMachine
  • changeServiceForVirtualMachine
  • revertToVMSnapshot
  • listVirtualMachines
  • attachVolume
  • createVolume
  • detachVolume
  • migrateVolume
  • resizeVolume
  • updateVolume
  • uploadVolume
  • listVolumes
  • associateIpAddress
  • listPublicIpAddresses
  • createNetwork
  • updateNetwork
  • listNetworks
  • createVPC
  • listVPCs
  • updateVPC

For APIs not listed above, response will be the same for all the users(root admin, domain admin, normal user) as usual, by default, it is full response view.

IAM Schema

IAM API

New API's

  • createIAMGroup
    1. String name - name of the IAM group. Required
    2. String description - short decsription
    3. String domainId - UUID of the domain of the account owning the IAM group
  • deleteIAMGroup
    1. String id - UUID of the IAM group. Required
  • listIAMGroups
    1. String name - name of the IAM group.
    2. String id - UUID of the IAM group.
  • addAccountToIAMGroup
    1. String id - UUID of the IAM group. Required
    2. List<String> accounts - comma separated list of account id that are going to be assigned to the IAM group
  • removeAccountFromIAMGroup
    1. String id - UUID of the IAM group. Required
    2. List<String> accounts - comma separated list of account id that are going to be removed from the IAM group
  • createIAMPolicy
    1. String name - name of the IAM Policy. Required
    2. String description - short decsription
    3. String domainId - UUID of the domain of the account owning the IAM policy
    4. String sourcePolicyId - UUID of the policy which should be used as a template to generate the new policy
  • deleteIAMPolicy
    1. String id - UUID of the IAM Policy. Required
  • listIAMPolicies
    1. String name - name of the IAM policy.
    2. String id - UUID of the IAM policy.
  • attachIAMPolicyToIAMGroup
    1. String id - UUID of the IAM group. Required
    2. List<String> policies - comma separated list of policy ids that are going to be assigned to the IAM group
  • removeIAMPolicyFromIAMGroup
    1. String id - UUID of the IAM group. Required
    2. List<String> policies- comma separated list of policy ids that are going to be revoked from the IAM group
  • attachIAMPolicyToAccount
    1. String id - UUID of the IAM policy. Required
    2. List<String> accounts - comma separated list of account id that the policy will attach to

  • removeIAMPolicyFromAccount
    1. String id - UUID of the IAM group. Required
    2. List<String> accounts- comma separated list of account ids 
  • addIAMPermissionToIAMPolicy
    1. String id - UUID of the IAM policy. Required
    2. String action - name of the API allowed/denied. Required
    3. String entityType
    4. String scope- ("Account" / "Domain" / "Resource")
    5. String scope id - UUID of the Scope
  • removeIAMPermissionFromIAMPolicy
    1. String id - UUID of the IAM policy. Required
    2. String action - name of the API allowed/denied. Required
    3. String entityType
    4. String scope- ("Account" / "Domain" / "Resource")
    5. String scope id - UUID of the Scope

 

In Phase1, these new IAM APIs are only limited to root admin.

IAM Interface

IAM Interface to check Entity Access

CloudStack currently has a domain-tree based implementation of access checks, namely com.cloud.acl.DomainChecker. This implementation is based on an adapter interface of Cloudstack - org.apache.cloudstack.acl.SecurityChecker that defines the basic IAM interface to check ownership and access control to objects within the account/ domain.

The IAM plugin will provide another implementation 'PolicyBasedAccessChecker' of the SecurityChecker interface. We will also have to add to this interface some more methods or change some signatures to facilitate group, policy and api name (action) based access control.

Code Block
/**
* SecurityChecker checks the ownership and access control to objects within
*/
public interface SecurityChecker extends Adapter {

...

/**
* Checks if the account can access the object.
*
* @param caller
* account to check against.
* @param entity
* object that the account is trying to access.
* @param accessType
*
* @param action
*
* @return true if access allowed. false if this adapter cannot provide permission.
* @throws PermissionDeniedException
* if this adapter is suppose to authenticate ownership and the check failed.
*/
boolean checkAccess(Account caller, ControlledEntity entity, AccessType accessType, String action) throws PermissionDeniedException;

....
}

/**
* Enumeration type for AccessType
*/
public enum AccessType {
	ModifyProject,
    OperateEntry,
    UseEntry,
	ListEntry
 }
AccessType
  • Until 4.4, CloudStack did not distinguish between a read-only access Vs read-and-use access Vs operate access. 
  • CloudStack access control layer always checked if the caller is owner of the entity and granted all types of access based on that. 
  • With IAM feature following are the types of entity access one can specify:
    1. ListEntry  (read only access)
    2. UseEntry  (read and use access)
    3. OperateEntry (operate/execute access)

Example: A domainAdmin registers a template T and allows a regular user of the domain to launch a VM using that template.

Entity: TemplateT
Principal1:  domainAdmin, Access allowed: OperateEntry   (operate access since he can invoke delete/updatepermissions operations on the template)
Principal2: normal domain user, Access allowed: UseEntry  (the user can only list the template and use it for launching VM)

 

 

The IAM implementation will check if a given user is permitted to invoke the given 'action' / 'accesstype' on the given resource by looking at the account's groups and the associated policies of those groups.

In phase I, all the permissions attached to any policy are by default explicit 'Allow' permissions. As of now 'Deny' permissions cannot be added.

Thus For given user, resource and given api name/accessType, default permission is 'deny', then run through this:

  • Find all groups the user belongs too.
  • Find all 'effective' policies the groups are associated to. Effective includes:
    1. All policy associations in the DB for the given account Id
    2. All policy associations in the DB for the groups that the given account Id is associated to
    3. All 'recursive= true' policy associations in the DB for the parent groups of the groups the  given account Id is associated to. Parent group is found out form the 'path' property of a group.
  • If any policy has a permission attached that 'Allows' the API, grant permission to make this call
  • Else, if no Allow entry is found for any policy for this API, deny the permission
What is 'recursive = true' permission?
  • This flag indicates a permission to an entity which is accessible in a group hierarchy downwards upto the 'leaf' group. The hierarchy is defined using the 'path' property of the group.
  • This design is used to map the 'domain' hierarchy of CloudStack and facilitate access check for entities that span the domain tree like Network, AffinityGroup.
  • This property 'recursive' is not exposed in IAM APIs, but only used by CloudStack orchestration when such domain-wide resources are created in the system through normal CS APIs.

IAM Interface to check API Access

IAM Plugin 'PolicyBasedAccessChecker' will implement the APIChecker interface.

Code Block
// APIChecker checks the ownership and access control to API requests
public interface APIChecker extends Adapter {
    // Interface for checking access for a role using apiname
    // If true, apiChecker has checked the operation
    // If false, apiChecker is unable to handle the operation or not implemented
    // On exception, checkAccess failed don't allow
    boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException;
}

The API permissions are also stored in the same db schema as the entity permissions. Since the above entity access already checks if the user is allowed to invoke the given API for the given resource, we need not have one more check just to see if the user can invoke the API.

So PolicyBasedAccessChecker :: checkAccess(User user, String apiCommandName), can return true always and rely on the entity based access check.

IAM Interface to facilitate Query APIs

Besides SecurityChecker and APIChecker interface, IAM plugin will also implement another QuerySelector interface to allow CloudStack to do proper row filter in ListAPI based on caller's policy. In this phase, we only support explicitly grant permission, not deny permission.

Code Block
/**
* QuerySelector returns granted domain, or account or resources for caller.
*/
public interface QuerySelector extends Adapter {

...

/**
* List granted domains for the caller, given a specific entity type.
*
* @param caller account to check against.
* @param entityType entity type
* @param accessType access type
* @return list of domain Ids granted to the caller account.
*/
List<Long> getAuthorizedDomains(Account caller, String entityType, AccessType accessType);

/**
* List granted accounts for the caller, given a specific entity type.
*
* @param caller account to check against.
* @param entityType entity type
* @param accessType access type
* @return list of account Ids granted to the caller account.
*/
List<Long> getAuthorizedAccounts(Account caller, String entityType, AccessType accessType);


/**
* List granted resources for the caller, given a specific entity type.
*
* @param caller account to check against.
* @param entityType entity type
* @param accessType access type
* @return list of resource Ids granted to the caller account.
*/
List<Long> getAuthorizedResources(Account caller, String entityType, AccessType accessType);

/**
 * Check if this account is associated with a policy with scope of ALL
 * @param caller account to check
 * @param action action.
 * @param accessType access type
 * @return true if this account is attached with a policy for the given action of ALL scope.
 */
boolean isGrantedAll(Account caller, String action, AccessType accessType);

/**
 * List of IAM group the given account belongs to
 * @param accountId account id.
 * @return IAM group names
 */
List<String> listIAMGroupsByAccount(long accountId); 

By invoking these QuerySelector APIs, CloudStack API engine can pre-construct proper SQL where clause to achieve proper row filter for accessibility control.

Component Design

IAM feature will be making use of CloudStack's plugin framework and will be implemented as a separate plugin. The end goal is to be able to host the IAM server as an independent service listening at an endpoint which the CS API service calls to do access checks.

IAM plugin will be developed under cloudstack-services project so that in future it can be easily separated as a service.

Following is the code base structure for the IAM feature:

 

 

cloud-iam service will be having two parts:

  •  plugin
    • This will be a CloudStack plugin and will integrate with CloudStack API/Server projects for all IAM related functionality through adapter implementations that get injected in the core CS components.
    • Contains implementations of all the IAM related adapter interfaces of CloudStack namely, SecurityChecker, APIChecker and the new QuerySelector
    • Contains the new IAM APIs related to group/policy/permission CRUD operations and APIS related to account-group associations.
  •  server
    • This will be an implementation of the pure IAM/RBAC model and is independent of any CloudStack terminology
    • Contains IAM interfaces dealing with group-policy-permission
    • Contains a GenericDao based implementation of these interfaces
    • Thus in order to work with a different IAM implementation say LDAP/AD based, one will have to implement the IAM server interfaces. The plugin portion and its integration with CS will not be affected.

 

Following component diagram illustrates these boundaries:

Gliffy Diagram
nameIAM_component_deployment

 

Response View

Currently CloudStack provides different response views for Root admin and non-root user, some response fields are only visible to root admin. Basically we have provided two static response views (Full view and Restricted view), domain admin will also have a User view. With new IAM service introduced, we should ideally also allow customers to be able to specify what view should be applied to each API when they are granting each API action to an IAM policy. To achieve that, we will implement as follows:

  1. For those APIs listed in previous Response View (Column Filter) section, when user grants them to a policy, we should allow them to pick one of two static response views (Full view and Restricted view) that are pre-defined. Note that we are not going to support full-fledged column filter (that is, allowing users to pick arbitrary columns to be see for each API) and only support static view association at the API level. In this release, support for granting with response view is not supported, and we still maintain the current cloudstack behavior where only root admin can see the Full view. This step will be delayed to Phase II.
  2. We will separate all current both admin and user allowed API commands to two classes: API for admin and API for user. For example, previous ListVMsCmd will be splitted into two classes: ListVMsCmdByAdmin and ListVMsCmd.

    Code Block
    @APICommand(name = "listVirtualMachines", description = "List the virtual machines owned by the account.", responseObject = UserVmResponse.class, responseView = ResponseView.Restricted, 
    entityType = { IAMEntityType.VirtualMachine })
    public class ListVMsCmd extends BaseListTaggedResourcesCmd {
    ......
    }
    
    @APICommand(name = "listVirtualMachines", description = "List the virtual machines owned by the account.", responseObject = UserVmResponse.class, responseView = ResponseView.Full)
    public class ListVMsCmdByAdmin extends ListVMsCmd {
        /////////////////////////////////////////////////////
        //////////////// API parameters /////////////////////
        /////////////////////////////////////////////////////
    
        @Parameter(name=ApiConstants.HOST_ID, type=CommandType.UUID, entityType=HostResponse.class,
                description="the host ID")
        private Long hostId;
    
        @Parameter(name=ApiConstants.POD_ID, type=CommandType.UUID, entityType=PodResponse.class,
                description="the pod ID")
        private Long podId;
    
        @Parameter(name=ApiConstants.STORAGE_ID, type=CommandType.UUID, entityType=StoragePoolResponse.class,
                description="the storage ID where vm's volumes belong to")
        private Long storageId;
    }
    

    Note that ListVMsCmdByAdmin and ListVMsCmd are sharing the same API name "listVirtualMachines". From client perspective, this is transparent to them, CloudStack API client will still just invoke previous listVirtualMachine API, and CloudStack API server will internally consult with IAM plugin to determine the group associated with the invoking user and then determine which internal Cmd class to be invoked. There is a new attribute "responseView" introduced for @APICommand annoation, which can be used to instruct CloudStack to generate different response view. By separating the command class into admin cmd class and user cmd class, we can also restrict valid input parameters for different account. For example, in this case, when Admin invokes listVirtualMachine api, he/she can pass hostId, podId and storageId, which parameters are not applicable for end user.

Default Policy

Default Policy Generation Flow

For backward compatibility, out-of-box, we provided 3 default policies: Root admin policy, domain admin policy, and end user policy. The process to populate permissions for these default policies are as follows:

  1. Parse all API command classes to check if they are annotated with "authorized" attribute. If so, populate a permission entry for that API for the default policy mapped to that authorized role.
    1. "authorized" attribute is an already existing attribute of our API's that is used to override the default access permissions defined in commands.properties file.
  2. Parse commands.property, create or override permission entries for each API for default policy mapped to API authorized roles.

When user creates a customized policy, he can specify a source policy from list of default policies. Then we will create new mapping between these default policy permission entries and the new policy in iam_policy_permission_map table.

Sample DB Entries for Default Policy

account

id

account_name

uuid

domain_id

2

admin

1c5afd64-482b-11e3-86f3-8118f47f9f9f

1

3

domainAdmin

929d172c-b95e-4b86-9474-9789072c9bdb

2

4

domainUserA

f96ddb47-d3c0-4360-a9cd-613d631c8333

2

iam_group

id

name

description

uuid

path

removed

created

1

REGULAR_USER

Domain user group

d283d4f0-31f0-11e3-ad37-80f85ce25918

/

NULL

2013-10-10 14:13:34

2

ADMIN

Root admin group

d283de28-31f0-11e3-ad37-80f85ce25918

/

NULL

2013-10-10 14:13:34

3

DOMAIN_ADMIN

Domain admin group

d283e6e8-31f0-11e3-ad37-80f85ce25918

/

NULL

2013-10-10 14:13:34

iam_group_account_map

id

group_id

account_id

removed

created

2

2

2

NULL

2013-10-10 14:13:34

3

3

3

NULL

2013-10-11 00:14:54

4

1

4

NULL

2013-10-11 00:19:55

iam_policy

id

name

description

uuid

path

removed

created

policy_type

1

REGULAR_USER

Domain user role

d2838dce-31f0-11e3-ad37-80f85ce25918

/

NULL

2013-10-10 14:13:34

Static

2

ADMIN

Root admin role

d2839c56-31f0-11e3-ad37-80f85ce25918

/

NULL

2013-10-10 14:13:34

Static

3

DOMAIN_ADMIN

Domain admin role

d283a7f0-31f0-11e3-ad37-80f85ce25918

/

NULL

2013-10-10 14:13:34

Static

iam_group_policy_map

id

group_id

policy_id

removed

created

1

1

1

NULL

2013-10-10 14:13:34

2

2

2

NULL

2013-10-10 14:13:34

3

3

3

NULL

2013-10-10 14:13:34

Sample DB entries for the policy permissions for 'StartVM' operation:

iam_permission

id

action

resource_type

scope_id

scope

access_type

permission

removed

created

1

startVirtualMachine

VirtualMachine

NULL

ALL

NULL

Allow

NULL

2013-10-10 14:13:34

2

startVirtualMachine

VirtualMachine

$domainId

Domain

NULL

Allow

NULL

2013-10-10 14:13:34

3

startVirtualMachine

VirtualMachine

$accountId

Account

NULL

Allow

NULL

2013-10-10 14:13:34

iam_policy_permission_map

id

policy_id

permission_id

removed

created

1

1

3

NULL

2013-10-10 14:13:34

2

2

1

NULL

2013-10-10 14:13:34

3

3

2

NULL

2013-10-10 14:13:34

Access Check Flow

Lets consider the StartVM API is being called by a user and run through the access control usecase for each default policy.

The StartVMCmd will contain an annotation on the field that needs to be checked for access:

Code Block
@APICommand(name = "startVirtualMachine", responseObject = UserVmResponse.class, description = "Starts a virtual machine.", entityType = { IAMEntityType.VirtualMachine })
public class StartVMCmd extends BaseAsyncCmd {
    public static final Logger s_logger = Logger.getLogger(StartVMCmd.class.getName());

    private static final String s_name = "startvirtualmachineresponse";

    // ///////////////////////////////////////////////////
    // ////////////// API parameters /////////////////////
    // ///////////////////////////////////////////////////

    @ACL
    @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType=UserVmResponse.class,
            required = true, description = "The ID of the virtual machine")
    private Long id;

Regular user 'domainUserA' calls this command for his own VM:

API access check

The API layer will call the APICheckers to see if the user is allowed to invoke this API. The PolicyBasedAccessChecker :: checkAccess(user, apiName) will just return true and rely on next step

Entity Access Check

The @ACL annotation invokes the SecurityChecker implementation. The PolicyBasedAccessChecker:: checkAccess(Account caller, ControlledEntity entity, AccessType accessType, String action) is invoked for the given user account and VM Id for action 'startVirtualMachine'

  • Find all groups the user belongs to: groupIDs = 1
  • Find all 'Effective' policies the groups are associated to: policies = 1
  • If any policy 'Allows' the startVirtualMachine API for this Vm Id, grant permission to make this call: Policy Id 1 and Permission Id 3 allow the API to be invoked for this user.
  • In this case, since this is a regular user and the VM belongs to the "ACCOUNT" scope of the user, then he is granted access using policy Id 1.

A Domain Admin 'domainAdmin' calls this command for a VM in his domain:

Entity Access Check

  • Find all groups the user belongs to: groupIDs = 3
  • Find all 'Effective' policies the groups are associated to: policies = 3
  • Policy Id 3 and Permission Id 2 allow 'startVirtualMachine' access for VMs in the 'Domain' scope - VMs in the domainId of the user.
  • In this case, since the VM is in the domain of the user, he is granted access using policy Id 3.

A Root Admin 'admin' calls this command for any VM:

Entity Access Check

  • Find all groups the user belongs to: groupIDs = 2
  • Find all 'Effective' policies the groups are associated to: policies = 2
  • Policy Id 2 and Permission Id 1 allow 'startVirtualMachine' access for ALL VMs .

@ACL annotation changes

  • As illustrated in the above access flow, the access checks get invoked when the resource Ids in the API Cmd are annotated.
  • Thus we will have to edit all existing API Cmds and add the relevant @ACL annotation on the primary resource Ids the command operates on.
  • For any other resources that the command works with, the current access checks placed in the service layer will invoke the SecurityChecker.
  • These current access checks pass the AccessType whereever needed or mostly pass null. Our SecurityChecker will interpret null as a 'UseEntry' access. In the 'iam_permission' schema, all List* APIs will be marked as 'UseEntry' AccessType entries to facilitate this access check.

Example: Creating Custom Group and Policy

Consider following example:

A Domain admin wants to create a 'Service Desk' group for his domain and allow 'ready only' access to the group for all VMs and Volumes within the Domain.

Steps:

  • createIAMGroup('Service Desk', 'Service Desk group', $domainId of the admin)
  • createIAMPolicy('Read Only Access', 'read only access to domain resources', $domainId of the admin)
  • createIAMPermission('ListVirtualMachine', 'Allow', 'Domain', $domainId, 'VirtualMachine')
  • createIAMPermission('ListVolumes', 'Allow', 'Domain', $domainId, 'Volume')
  • addIAMPermissionToIAMPolicy( UUID of the 'Read Only Access' policy, List<String> permissionIds of above permissions)
  • attachIAMPolicyToIAMGroup (groupId, policyId)
  • addAccountToIAMGroup(groupId, List<String> accountIds)

NOTE:

For this release, creating a custom policy/group is supported through API only.  For further releases, we can provide either a UI or a config file + policy language mechanism to facilitate the custom policy/group creation.

Presentation at CloudStack Collaboration Conference

ApachIAM.pptx