Beskyttede data
Slik setter du opp ekstra databeskyttelse for en app
Beskyttede data er informasjon som krever ekstra tilgangskontroll, for eksempel personopplysninger eller konfidensiell/klassifisert informasjon. Les mer om konseptet beskyttede data.
Sett opp Maskinporten
Du må sette opp Maskinporten for at appen skal kunne utføre handlinger på vegne av tjenesteeier. Se integrere en Altinn-app med Maskinporten.
Sett opp datatyper
Filen applicationmetadata.json definerer alle datatyper (kun på engelsk foreløpig) i en app. Her angir du hvilke handlinger som kreves for den beskyttede datatypen.
I dette eksempelet setter du opp en ny datatype der du angir egenskapene actionRequiredToRead og actionRequiredToWrite, og deaktiverer autoCreate. Du 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, får du en 403-Forbidden-feil.Sett opp autorisasjonspolicy
Ta utgangspunkt i standard policy.xml-fil, og endre regel #2 for å gi tjenesteeieren 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 skrive all relevant logikk manuelt.
I denne delen oppretter du en tjeneste som hjelper deg å samhandle med beskyttede data, før du ser hvordan du kan opprette, endre og lese beskyttede dataelementer i en vanlig prosessflyt.
Lag en hjelpetjeneste
For å forenkle autorisasjon og samhandling med den beskyttede datamodellen, kan du 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));
}Du kan registrere denne tjenesten i Program.cs og bruke den med dependency injection der du trenger den.
App/Program.csvoid RegisterCustomAppServices(IServiceCollection services, IConfiguration config, IWebHostEnvironment env)
{
// ...
services.AddTransient<RestrictedDataHelper>();
}Skriv data
Som nevnt tidligere, må du manuelt opprette dataelementet når appen går inn i prosessteget Task_1.
Eksempelet under gjør dette i en prosess-hook som implementerer IOnTaskStartingHandler. Den henter informasjon fra et fiktivt API og lagrer den i den beskyttede datamodellen. Hooken skriver gjennom context.InstanceDataMutator, som plattformen lagrer når hooken er ferdig, så du trenger ikke RestrictedDataHelper her. Informasjonen er ikke tilgjengelig for brukeren, men appen kan hente den senere.
App/logic/ProcessTaskStartHandler.cspublic class ProcessTaskStartHandler(ISomeTaxService someTaxService) : IOnTaskStartingHandler
{
/// <summary>
/// Runs when the process enters task step "Task_1"
/// </summary>
public bool ShouldRunForTask(string taskId) => taskId == "Task_1";
public async Task<HandlerResult> Execute(OnTaskStartingContext context)
{
var dataMutator = context.InstanceDataMutator;
var taxPrefill = await someTaxService.GetTaxPrefillData(dataMutator.Instance);
var spouse = new Spouse
{
Name = taxPrefill.Spouse?.Name,
NationalIdentityNumber = taxPrefill.Spouse?.NationalIdentityNumber,
GrossIncome = taxPrefill.Spouse?.Income,
GrossDebt = taxPrefill.Spouse?.Debt,
};
// The hook may run more than once, so update the element if it already exists
var existing = dataMutator.GetDataElementsForType("restrictedDataModel").FirstOrDefault();
if (existing is null)
{
dataMutator.AddFormDataElement("restrictedDataModel", new RestrictedDataModel { Spouse = spouse });
}
else
{
var restrictedData = await dataMutator.GetFormData<RestrictedDataModel>(existing);
restrictedData.Spouse = spouse;
}
return HandlerResult.Success();
}
}Les data
I koden under lager du en implementasjon av IDataWriteProcessor-grensesnittet, der du utfører en fiktiv skatteberegning. Denne beregningen krever informasjon du tidligere har lagret i den beskyttede datamodellen, så du bruker RestrictedDataHelper.GetOrCreateData for å hente den.
App/logic/DataWriteHandler.cspublic 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;
}
}