10

As I understand the Salesforce FMA, you can enable certain Boolean toggles per LMA license. But those licences are per org, either:

  1. As Site-wide license for all users of a subscriber org
  2. An amount of seats / users per subscriber org

Even for 2. there is only ONE license record in the LMA for which I can enable a feature.

How can simulate per-user feature licences? I mean sell 3 Features to a Subscriber which has a Site-Wide or n-seat license.

And how would the subscriber orgs Admin then assign those "feature licenses" to concrete org users?

I see some possible solutions but would like to learn what the recommended approach is and why:

  • Use an Integer LMO-to-Subscriber Feature parameter
  • Use Protected Custom Settings and Grant Login Access

2 Answers 2

5
+100
  • In salesforce, access is controlled through Permission sets.
  • LMO org can assign how many feature-specific licenses are assigned using an FMA Integer parameter,
  • Currently there is no standard way to limit license specific permission set assignments

here is the approach I would take to breeze this gap

  1. Create an Integer FMA parameter and permission set combination for each feature.
  2. Assign seats for Features in LMO using FMA parameter
  3. An apex class which will check if the logged-in user
    • has permission set assigned
    • Current users permission set assignment is among the first num_SeatsPermX permission set assignment
  4. When app code needs feature it calls CurrentUser.hasAccess(new Feature('FeatureX', 'FeatureXPermissionSet')).

This will allow the LMO org to control how many users have access to a specific feature, Even if the subscriber tries to assign permission sets to users more than assigned seats any user who has been assigned permission set after the seats are filled will not have access to the feature

To improve performance we can leverage Platform Cache and store computed feature access in the cache.

Here is the sample code

public with sharing class CurrentUser {

    public static Boolean hasAccess(Feature feature) {
        Set<Id> assignedUsers = new Set<Id>();

        for(PermissionSetAssignment  permissionSetAssignment : [SELECT Id, AssigneeId 
                                                                 FROM PermissionSetAssignment 
                                                                 WHERE PermissionSetId = :feature.permissionSetId 
                                                                 ORDER By SystemModstamp 
                                                                 LIMIT :feature.seats]) {
            assignedUsers.add(permissionSetAssignment.AssigneeId);
        }

        return assignedUsers.contains(UserInfo.getUserId());
    }
}

public with sharing class Feature {
    public Id permissionSetId { get; private set; }
    public Integer seats { get; private set; }

    public Feature(String featureName, String permissionSetName) {
        permissionSetId = [SELECT Id FROM PermissionSet WHERE Name = :permissionSetName].Id;
        seats = System.FeatureManagement.checkPackageIntegerValue(featureName);
    }
}

if(!CurrentUser.hasAccess(new Feature('FeatureX', 'FeatureXPermissionSet'))) {
    throw new FeatureAccessException('You do not have access to this feature');
}
5
  • 2
    I like the notion of using the PermissionSetAssignment object for this, but the code could end up querying a lot of records so performance and governor limits would be a concern. But I guess the query could be changed. And I would want to include only users that are active. Thanks!
    – Keith C
    Apr 30, 2020 at 6:58
  • 1
    I have added a limit = feature.seats, so even in the worst case it will query limited records. I initially thought about adding an Active user check(where Assignee.IsActive = true) but it would make my SOQL query non-indexed impacting performance. Apr 30, 2020 at 7:10
  • 1
    Thanks Amol. We have user counts in the thousands so querying thousands of rows seems a bad idea.
    – Keith C
    Apr 30, 2020 at 7:55
  • @KeithC If you use Platform Cache you can query them seldom and access them fast. In a single transaction you also only need to know if the current user has the assignment. That's not performance relevant, I guess. May 6, 2020 at 9:12
  • How does the purchasing process look like with this approach? Would there be a base license on the AppExchange for the app, with additional features requested separately by subscribers? Also does this approach adhere to the AppExchange revenue sharing policy?
    – Mossi
    Aug 4, 2020 at 19:27
1

I'd like to know how to do this too - see How to use the Feature Management App (FMA) to grant access for a defined number of users?

What I plan to do if nothing better comes up, is have a protected list custom setting holding the User Id as a text string plus the feature name as a text string. The presence of a record means access is granted and the lack of a record means it isn't.

As far as the allocation goes, I intend for it to be first come, first served. If the available seats are all gone when a user tries to access a feature, they will either get an error message or the feature may just not appear in their UI.

Something like this where FeatureAssignment__c is the protected list custom setting:

// Worth caching if multiple check calls in a transaction
private static final Map<String, Boolean> cache = new Map<String, Boolean>();

public static Boolean checkFeatureAvailable(String featureName) {

    Boolean available = cache.get(featureName);
    if (available != null) {
        return available;
    } else {
        Boolean assigned = queryFeatureAvailable(featureName);
        cache.put(featureName, assigned);
        return assigned;
    }
}

// Adds a query so can't precede a callout
private static Boolean queryFeatureAvailable(String featureName) {

    Boolean assigned = [
        select count()
        from FeatureAssignment__c
        where UserId__c = :UserInfo.getUserId()
        and FeatureName__c = :featureName
    ] > 0;

    if (assigned) return true;
    else return assignFeature(featureName);
}

// Adds an additional query and a DML
private static Boolean assignFeature(String featureName) {

    Integer available = FeatureManagement.checkPackageIntegerValue(featureName);

    Integer used = [
        select count()
        from FeatureAssignment__c
        and FeatureName__c = :featureName
    ];

    if (used < available) {
        insert new FeatureAssignment__c(
            Name = ...,
            UserId__c = UserInfo.getUserId(),
            FeatureName__c = featureName
        );
        return true;
    } else {
        return false;
    }
}

plus a trigger to return access that is no longer being used to the available pool:

trigger UserTrigger on User (after update) {

    Set<Id> deactivatedUserIds = new Set<Id>();
    for (User u : Trigger.new) {
        User old = Trigger.oldMap.get(u.Id);
        if (old.IsActive && !u.IsActive) {
            deactivatedUserIds.add(u.Id);
        }
    }
    if (deactivatedUserIds.size() > 0) {
        // Free up the assignment (if any)
        delete [
            select Id
            from FeatureAssignment__c
            where UserId__c in :deactivatedUserIds
        ];
    }
}

This also allows an admin to free up the feature assignment by de-activating and re-activating the user.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .