Sist endret: 5. sep. 2025

Beskyttede data

Hvordan sette opp ekstra databeskyttelse for en app

Tilgjengelig fra v8.7.0

Introduksjon

Beskyttede data er informasjon som krever ekstra tilgangskontroll, for eksempel personopplysninger eller konfidensiell/klassifisert informasjon.

Du kan lese mer om konseptet her.

Konfigurasjon av Maskinporten

Du må konfigurere Maskinporten for at appen skal kunne utføre handlinger på vegne av tjenesteeier.

Du finner en detaljert veiledning for oppsettet her.

Konfigurasjon av datatyper

applicationmetadata.json-filen definerer alle datatyper i en applikasjon. Her angir du hvilke handlinger som kreves for din beskyttede datatype.

I dette eksempelet konfigurerer vi en ny datatype, hvor vi spesifiserer egenskapene actionRequiredToRead og actionRequiredToWrite, og deaktiverer autoCreate. Vi bruker identifikatoren restrictedDataModel, men navnet i seg selv er ikke viktig.

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"
}
Vi deaktiverer auto-create fordi vår oppdaterte autorisasjonspolicy ikke gir lese- eller skrivetilgang til sluttbrukere. Forsøk på å opprette et dataelement av typen restrictedDataModel med en brukers autorisasjonstoken vil resultere i en 403-Forbidden feil.

Konfigurasjon av autorisasjonspolicy

Ta utgangspunkt i standard policy.xml fil, og endre regel #2 for å gi tjenesteeier tilgang til de nye handlingene.

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>

Interaksjon med beskyttede data

Siden restrictedDataModel ikke opprettes automatisk eller er knyttet til brukerens normale dataflyt, må du implementere all relevant logikk manuelt.

I denne delen oppretter vi en tjeneste som hjelper oss å samhandle med beskyttede data, før vi viser hvordan vi kan opprette, endre og lese beskyttede dataelementer i en vanlig prosessflyt.

Hjelpetjeneste

For å forenkle autorisasjon og interaksjon med den beskyttede datamodellen, kan vi opprette en hjelpetjeneste som håndterer denne kompleksiteten.

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));
}

Denne tjensten kan registreres i Program.cs og brukes med dependency injection der den behøves.

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

  services.AddTransient<RestrictedDataHelper>();
}

Skriving av data

Som nevnt tidligere, må vi manuelt opprette dataelementet når applikasjonen går inn i prosessteget Task_1.

For å gjøre dette bruker du metoden UpdateOrCreateData fra RestrictedDataHelper-tjenesten.

Eksempelet under implementerer denne logikken i IProcessTaskStart-interfacet, hvor vi henter informasjon fra et fiktivt API og lagrer det i den beskyttede datamodellen. Denne informasjonen vil ikke være tilgjengelig for brukeren, men kan hentes senere av appen.

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
    );
  }
}

Lesing av data

I koden under har vi laget en implementasjon av IDataWriteProcessor-interfacet, hvor vi utfører en fiktiv skatteberegning. Denne beregningen krever informasjon vi tidligere har lagret i den beskyttede datamodellen, så vi bruker RestrictedDataHelper.GetOrCreateData for å hente den.

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;
  }
}