Beskyttede data
Hvordan sette opp ekstra databeskyttelse for en app
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"
}
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;
}
}