Core

Tenancy Manager Documentation

Overview

What is Tenancy in Software?

In software — especially cloud-based applications or SaaS (Software as a Service)tenancy refers to how multiple customers (tenants) use the same software system. Each tenant is usually a company or user group that uses the application independently of others.

What is Tenancy Management?

Tenancy management is about organizing, separating, and securing each tenant’s data and settings so they can all share the same system without interfering with each other.

Tenancy Management at Feedier

Types of Tenancy

The Feedier platform has two types of tenancies:

  • User
  • Team

The tenancy management allows you to define Model (database) access on a User and Team level.

For example: do we want to grant access on the same data for other Team members from the same team as the current User? In some cases yes, in others maybe not.

Let's dive into some code.

Each Model adds a Global Scope in the booted() method.

Note: Global Scopes are a concept by Laravel which allow you to add constraints to all queries for a given model.

Let's take the Feedback model as an example:

protected static function booted()
{
    static::addGlobalScope(FeedbackScope::class);
    static::addGlobalScope(RestrictedViewerScope::class);
}

As you can see, for the Feedback model there are two scopes added: a general FeedbackScope and a RestrictedViewerScope.

Important: If you create a new model it is very important to define the global scope of that model. You can lookup the implementation of existing scopes in

app\Models\Scopes\Tenancy

Tenancy context at Feedier

There are two context cases where Tenancy Management is relevant:

  • during HTTP Requests
  • when dispatching Jobs on a queue.

HTTP Requests

During the life cycle of an HTTP Request the Tenancy middleware is taking care of setting the correct Team and User tenancies in its handle method:

public function handle($request, Closure $next)
{
    match ($request->attributes->get('tenancy_by')) {
        Report::class   => $this->defineReport($request->reportTeam ?? $request->report->team, $request->reportUser),
        Feedback::class => $this->defineFeedback($request->feedback->team),
        ApiKey::class   => $this->defineApiKey($request->organization),
        default         => $this->defineAuthenticatedUser($request->user()),
    };

    return $next($request);
}

According to the request attribute tenancy_by, different tenancies for Team and User are being set.

The tenancy_by request attribute is set by other middlewares which are executed before the Tenancy middleware.

Let's look at the default case and the method defineAuthenticatedUser:

private function defineAuthenticatedUser(?User $user = null): void
{
    if (!$user) {
        return;
    }

    $tenancyManager = app(TenancyManagerInterface::class);

    $tenancyManager->setTeamTenancy($user->team);
    $tenancyManager->setUserTenancy($user);
}

It simply sets the Team tenancy from the team to which the user belongs to. And it sets the User tenancy.

Jobs / Queues

When dispatching a job it is very important to set the Team and User tenancy, as no middleware is involved during the job life cycle.

The following example shows how to set the tenancy in the handle method of a Job:

public function handle(): void
{
    $this->aiPopupAnalysisMission = $this->createAiPopupAnalysisMission($this->aiPopupAnalysisData);

    if (!$this->aiPopupAnalysisMission) {
       return;
    }

    if (!($team = $this->aiPopupAnalysisMission->getTeam())) {
       return;
    }

    $this->user = $this->aiPopupAnalysisMission->getUser();


    /** Set the Tenancy **/

    $tenancyManager = app(TenancyManagerInterface::class);
    $tenancyManager->setTeamTenancy($team);
    $tenancyManager->setUserTenancy($this->user);

    [...]
}

Both methods setTeamTenancy and setUserTenancy require a Team and User Model as the parameter.

It ensures that the Global Scope is correctly set for the execution of the Job.

Tenancy and Carrier Ids

If no Team Tenancy was set we make sure that no results are returned when using Elastic Search. The following code example returns an Elastic query where carrier_id is set to -1:

$builder = app(ElasticSearch::class)
    ->addFql($segmentFql)
    ->addSelector($feedbackFql)
    ->getBuilder();

The resulting query:

"query":{"bool":{"must":[{"terms":{"id":[195315]}},{"terms":{"carrier_id":[-1]}}]}},"track_total_hits":true}

Note: the -1 value has been introduced in order to avoid returning data when no Tenancy has been set.

This issue can be fixed by setting the Tenancy:

$tenancyManager = app(TenancyManagerInterface::class);
$tenancyManager->setTeamTenancy($feedback->team);
Previous
Autopilot