Last modified: Sep 5, 2025

Restricted data

How to set up additional data protections for an app

Available from v8.7.0

Introduction

Restricted data refers to information requiring additional protection, such as personal, confidential, or classified data.

You can read more about the concept here.

Configuring Maskinporten

You must configure Maskinporten to allow the app to perform actions on behalf of the service owner.

You can find a detailed guide on that setup here.

Configuring the data types

The applicationmetadata.json file defines all data types in an application. Here, you specify which actions are required for your restricted data type.

In this example, we configure a new data type, specifying the actionRequiredToRead and actionRequiredToWrite properties, and disabling autoCreate. We use the identifier restrictedDataModel, though the name itself is not significant.

App/config/applicationmetadata.json
{
  "id": "restrictedDataModel",
  "allowedContentTypes": ["application/xml"],
  "appLogic": {
    "autoCreate": false,
    "classRef": "Altinn.App.Models.RestrictedDataModel"
  },
  "taskId": "Task_1",
  "maxCount": 1,
  "minCount": 1,
  "actionRequiredToRead": "customActionRead",
  "actionRequiredToWrite": "customActionWrite"
}
We disable auto-create because our updated authorization policy does not grant read or write access to end-users. Attempting to create a restrictedDataModel data element with a user’s authorization token will result in a 403-Forbidden error.

Configuring the authorization policy

Using the default policy.xml file as a starting point, modify rule #2 to grant the new custom actions to bearers of a service owner token.

App/config/authorization/policy.xml
<xacml:Rule RuleId="urn:altinn:resource:app_[ORG]_[APP]:policyid:1:ruleid:2" Effect="Permit">
  <xacml:Description>Regel for tjenesteeier: Gir rettighetene Les, Skriv, Start, Bekreft mottatt tjenesteeier til organisasjonen som eier tjenesten ([org]).</xacml:Description>
  <xacml:Target>
    <xacml:AnyOf>
      <xacml:AllOf>
        <xacml:Match MatchId="urn:oasis:names:tc:xacml:3.0:function:string-equal-ignore-case">
          <xacml:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">[org]</xacml:AttributeValue>
          <xacml:AttributeDesignator AttributeId="urn:altinn:org" Category="urn:oasis:names:tc:xacml:1.0:subject-category:access-subject" DataType="http://www.w3.org/2001/XMLSchema#string" MustBePresent="false" />
        </xacml:Match>
      </xacml:AllOf>
    </xacml:AnyOf>
    <xacml:AnyOf>
      <xacml:AllOf>
        <xacml:Match MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
          <xacml:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">[ORG]</xacml:AttributeValue>
          <xacml:AttributeDesignator AttributeId="urn:altinn:org" Category="urn:oasis:names:tc:xacml:3.0:attribute-category:resource" DataType="http://www.w3.org/2001/XMLSchema#string" MustBePresent="false" />
        </xacml:Match>
        <xacml:Match MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
          <xacml:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">[APP]</xacml:AttributeValue>
          <xacml:AttributeDesignator AttributeId="urn:altinn:app" Category="urn:oasis:names:tc:xacml:3.0:attribute-category:resource" DataType="http://www.w3.org/2001/XMLSchema#string" MustBePresent="false" />
        </xacml:Match>
      </xacml:AllOf>
    </xacml:AnyOf>
    <xacml:AnyOf>
      <xacml:AllOf>
        <xacml:Match MatchId="urn:oasis:names:tc:xacml:3.0:function:string-equal-ignore-case">
          <xacml:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">read</xacml:AttributeValue>
          <xacml:AttributeDesignator AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id" Category="urn:oasis:names:tc:xacml:3.0:attribute-category:action" DataType="http://www.w3.org/2001/XMLSchema#string" MustBePresent="false" />
        </xacml:Match>
      </xacml:AllOf>
      <xacml:AllOf>
        <xacml:Match MatchId="urn:oasis:names:tc:xacml:3.0:function:string-equal-ignore-case">
          <xacml:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">write</xacml:AttributeValue>
          <xacml:AttributeDesignator AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id" Category="urn:oasis:names:tc:xacml:3.0:attribute-category:action" DataType="http://www.w3.org/2001/XMLSchema#string" MustBePresent="false" />
        </xacml:Match>
      </xacml:AllOf>
      <xacml:AllOf>
        <xacml:Match MatchId="urn:oasis:names:tc:xacml:3.0:function:string-equal-ignore-case">
          <xacml:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">customActionRead</xacml:AttributeValue>
          <xacml:AttributeDesignator AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id" Category="urn:oasis:names:tc:xacml:3.0:attribute-category:action" DataType="http://www.w3.org/2001/XMLSchema#string" MustBePresent="false" />
        </xacml:Match>
      </xacml:AllOf>
      <xacml:AllOf>
        <xacml:Match MatchId="urn:oasis:names:tc:xacml:3.0:function:string-equal-ignore-case">
          <xacml:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">customActionWrite</xacml:AttributeValue>
          <xacml:AttributeDesignator AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id" Category="urn:oasis:names:tc:xacml:3.0:attribute-category:action" DataType="http://www.w3.org/2001/XMLSchema#string" MustBePresent="false" />
        </xacml:Match>
      </xacml:AllOf>
      <xacml:AllOf>
        <xacml:Match MatchId="urn:oasis:names:tc:xacml:3.0:function:string-equal-ignore-case">
          <xacml:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">instantiate</xacml:AttributeValue>
          <xacml:AttributeDesignator AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id" Category="urn:oasis:names:tc:xacml:3.0:attribute-category:action" DataType="http://www.w3.org/2001/XMLSchema#string" MustBePresent="false" />
        </xacml:Match>
      </xacml:AllOf>
      <xacml:AllOf>
        <xacml:Match MatchId="urn:oasis:names:tc:xacml:3.0:function:string-equal-ignore-case">
          <xacml:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">complete</xacml:AttributeValue>
          <xacml:AttributeDesignator AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id" Category="urn:oasis:names:tc:xacml:3.0:attribute-category:action" DataType="http://www.w3.org/2001/XMLSchema#string" MustBePresent="false" />
        </xacml:Match>
      </xacml:AllOf>
    </xacml:AnyOf>
  </xacml:Target>
</xacml:Rule>

Interacting with the restricted data

Since the restrictedDataModel is not automatically created or attached to the user’s normal data flow, you must implement all relevant logic manually.

In this section we’ll create a service that helps us interact with the restricted data, before demonstrating how we can create, modify, and read restricted data elements in a normal app flow.

Helper service

To simplify authorization and interaction with the restricted data model, we can create a helper service to handle this complexity.

App/logic/RestrictedDataHelper.cs
/// <summary>
/// Note: The logic in this service assumes a single data element of a given data type.
/// </summary>
public class RestrictedDataHelper(IDataClient dataClient)
{
  /// <summary>
  /// Retrieves an existing data element of the specified type or creates a new one if it doesn't exist.
  /// </summary>
  public async Task<(T data, DataElement element)> GetOrCreateData<T>(
    string dataTypeName,
    Instance instance
  )
    where T : class, new()
  {
    var (instanceId, appId) = GetIdentifiers(instance);
    var dataElement = instance.Data.FirstOrDefault(x => x.DataType == dataTypeName);

    // Create a new data element
    if (dataElement is null)
    {
      var newData = new T();
      dataElement = await CreateDataElement(newData, dataTypeName, instanceId, appId);

      // Track the newly created data element with the instance
      instance.Data.Add(dataElement);

      return (newData, dataElement);
    }

    // Data element already exists, retrieve it
    var existingData = await GetModelData<T>(dataElement, instanceId, appId);

    return (existingData, dataElement);
  }

  /// <summary>
  /// Updates an existing data element of the specified type or creates a new one if it doesn't exist.
  /// </summary>
  public async Task<DataElement> UpdateOrCreateData<T>(
    T data,
    string dataTypeName,
    Instance instance
  )
    where T : class, new()
  {
    var (instanceId, appId) = GetIdentifiers(instance);
    var dataElement = instance.Data.FirstOrDefault(x => x.DataType == dataTypeName);

    // Create a new data element
    if (dataElement is null)
    {
      dataElement = await CreateDataElement(data, dataTypeName, instanceId, appId);

      // Track the newly created data element with the instance
      instance.Data.Add(dataElement);

      return dataElement;
    }

    // Data element already exists, update it
    return await UpdateModelData(data, dataElement, instanceId, appId);
  }

  private async Task<DataElement> CreateDataElement<T>(
    T data,
    string dataTypeName,
    InstanceIdentifier instanceId,
    AppIdentifier appId
  )
    where T : class =>
    await dataClient.InsertFormData(
      data,
      instanceId.InstanceGuid,
      typeof(T),
      appId.Org,
      appId.App,
      instanceId.InstanceOwnerPartyId,
      dataTypeName,
      authenticationMethod: StorageAuthenticationMethod.ServiceOwner()
    );

  private async Task<T> GetModelData<T>(
    DataElement dataElement,
    InstanceIdentifier instanceId,
    AppIdentifier appId
  )
    where T : class =>
    await dataClient.GetFormData(
      instanceId.InstanceGuid,
      typeof(T),
      appId.Org,
      appId.App,
      instanceId.InstanceOwnerPartyId,
      Guid.Parse(dataElement.Id),
      authenticationMethod: StorageAuthenticationMethod.ServiceOwner()
    ) as T
    ?? throw new InvalidCastException(
      $"Data element {dataElement.Id} is not of type {typeof(T)}"
    );

  private async Task<DataElement> UpdateModelData<T>(
    T data,
    DataElement dataElement,
    InstanceIdentifier instanceId,
    AppIdentifier appId
  )
    where T : class =>
    await dataClient.UpdateData(
      data,
      instanceId.InstanceGuid,
      typeof(T),
      appId.Org,
      appId.App,
      instanceId.InstanceOwnerPartyId,
      Guid.Parse(dataElement.Id),
      authenticationMethod: StorageAuthenticationMethod.ServiceOwner()
    );

  private static (InstanceIdentifier instanceId, AppIdentifier appId) GetIdentifiers(
    Instance instance
  ) => (new InstanceIdentifier(instance), new AppIdentifier(instance));
}

This service can then be registered in Program.cs and injected wherever you need it.

App/Program.cs
void RegisterCustomAppServices(IServiceCollection services, IConfiguration config, IWebHostEnvironment env)
{
  // ...

  services.AddTransient<RestrictedDataHelper>();
}

Writing data

As mentioned, we need to manually create the data element when the application enters the Task_1 process step.

To do this, use the UpdateOrCreateData method from the RestrictedDataHelper service.

The following example implements this logic in the IProcessTaskStart interface, fetching information from a fictional API and storing it in the restricted data model. This information remains unavailable to the user but can be retrieved later by the app.

App/logic/ProcessTaskStartHandler.cs
public class ProcessTaskStartHandler(
  RestrictedDataHelper restrictedDataHelper,
  ISomeTaxService someTaxService
) : IProcessTaskStart
{

  /// <summary>
  /// This method will execute when the process enters task step "Task_1"
  /// </summary>
  public async Task Start(string taskId, Instance instance, Dictionary<string, string> prefill)
  {
    if (taskId != "Task_1")
      return;

    var taxPrefill = await someTaxService.GetTaxPrefillData(instance);
    var restrictedData = new RestrictedDataModel
    {
      Spouse = new Spouse
      {
        Name = taxPrefill.Spouse?.Name,
        NationalIdentityNumber = taxPrefill.Spouse?.NationalIdentityNumber,
        GrossIncome = taxPrefill.Spouse?.Income,
        GrossDebt = taxPrefill.Spouse?.Debt,
      }
    };

    await restrictedDataHelper.UpdateOrCreateData(
        restrictedData,
        "restrictedDataModel",
        instance
    );
  }
}

Reading data

In the following code, we have created an implementation of the IDataWriteProcessor interface, where we perform a fictional tax calculation. This calculation requires information we previously stored in the restricted data model, so we use RestrictedDataHelper.GetOrCreateData to retrieve it.

App/logic/DataWriteHandler.cs
public class DataWriteHandler(
  RestrictedDataHelper restrictedDataHelper,
  ISomeTaxService someTaxService
) : IDataWriteProcessor
{

  /// <summary>
  /// This method will execute when the user updates the income portion of the form
  /// </summary>
  public async Task ProcessDataWrite(
    IInstanceDataMutator instanceDataMutator,
    string taskId,
    DataElementChanges changes,
    string? language
  )
  {
    var formChanges = changes.FormDataChanges.FirstOrDefault(x =>
      x.DataType.Id == "dataModel"
    );

    if (formChanges is null)
      return;

    var previousData = formChanges.PreviousFormData as MainDataModel;
    var currentData = formChanges.CurrentFormData as MainDataModel;

    if (currentData is null || currentData.Income.Equals(previousData?.Income))
      return;

    var (restrictedData, _) = await restrictedDataHelper.GetOrCreateData<RestrictedDataModel>(
      "restrictedDataModel",
      instanceDataMutator.Instance
    );

    var taxRate = await someTaxService.GetTaxRateForHousehold(
      currentData.Income,
      restrictedData.Spouse,
      instanceDataMutator.Instance
    );

    currentData.TaxRate = taxRate.CalculatedRate;
  }
}