Skip to content

Commit

Permalink
Paranna auditlokien näyttämisessä käytettävää rajapintaa;
Browse files Browse the repository at this point in the history
Ota huomioon pää- ja sivuoppijaoidit.

Käsittele DynamoDB:n paluuarvoista kaikki sivut, eikä vain ensimmäistä, jonka maksimikoko on 1 MB.

Palauta tiedot kattavammin eri audit-log -entryistä. Mahdollista suoritusjakolinkkien ja OAuth2-rajapinnan käyttöjen raportointi. Plaauta tiedot myös YTR-datan ja muutoshistorian katselusta.
  • Loading branch information
AleksiAhtiainen committed Jan 2, 2025
1 parent 59895ac commit 20db8f3
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 34 deletions.
2 changes: 2 additions & 0 deletions src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,15 @@ mydata = {
purpose = "Tietoja käytetään opiskelijahintaisten matkalippujen myöntämiseen."
membercodes = ["2769790-1"] # Identify API caller
subsystemcodes = ["koski"] # Unused
orgOid = "1.2.246.562.10.77876988401" # Mydata use is interpreted based on this from auditlogs
},
{
id = "frank"
name = "Frank"
purpose = ""
membercodes = ["2769790-2"]
subsystemcodes = ["koski"]
orgOid = "1.2.246.562.10.46399742280"
},
]
callbackURLs = [
Expand Down
5 changes: 5 additions & 0 deletions src/main/scala/fi/oph/koski/mydata/MyDataConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ trait MyDataConfig extends Logging {
)
}

def isMyDataOrg(orgOid: String): Boolean = {
conf.getConfigList("members").asScala.exists(member =>
member.getString("orgOid") == orgOid
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ object AuditLogMockData extends Logging {
organizationOid = List(MockOrganisaatiot.helsinginKaupunki, MockOrganisaatiot.stadinAmmattiopisto),
raw = rawAuditlog("OPISKELUOIKEUS_KATSOMINEN")
),
MockData(
studentOid = KoskiSpecificMockOppijat.ylioppilas.oid,
time = "2019-05-19T11:21:42.123+03",
organizationOid = List(MockOrganisaatiot.helsinginKaupunki),
raw = rawAuditlog("YTR_OPISKELUOIKEUS_KATSOMINEN")
),
MockData(
studentOid = KoskiSpecificMockOppijat.ylioppilas.oid,
time = "2019-05-19T11:21:42.123+03",
organizationOid = List(MockOrganisaatiot.stadinAmmattiopisto),
raw = rawAuditlog("MUUTOSHISTORIA_KATSOMINEN")
),
MockData(
studentOid = KoskiSpecificMockOppijat.amis.oid,
time = "2020-01-12T20:31:32.104+03",
Expand Down Expand Up @@ -154,7 +166,56 @@ object AuditLogMockData extends Logging {
time = "2000-01-12T20:31:32.104+03",
organizationOid = List(Opetushallitus.organisaatioOid),
raw = rawAuditlog("OPISKELUOIKEUS_KATSOMINEN")
)
),
MockData(
studentOid = KoskiSpecificMockOppijat.ylioppilasLukiolainen.oid,
time = "2000-01-12T20:31:32.104+03",
organizationOid = List(MockOrganisaatiot.helsinginKaupunki),
raw = rawAuditlog("KANSALAINEN_SUORITUSJAKO_KATSOMINEN")
),
MockData(
studentOid = KoskiSpecificMockOppijat.ylioppilasLukiolainen.oid,
time = "2000-01-13T20:31:32.104+03",
organizationOid = List(MockOrganisaatiot.helsinginKaupunki),
raw = rawAuditlog("KANSALAINEN_SUORITUSJAKO_KATSOMINEN_SUORITETUT_TUTKINNOT")
),
MockData(
studentOid = KoskiSpecificMockOppijat.ylioppilasLukiolainen.oid,
time = "2000-01-14T20:31:32.104+03",
organizationOid = List(MockOrganisaatiot.helsinginKaupunki),
raw = rawAuditlog("KANSALAINEN_SUORITUSJAKO_KATSOMINEN_AKTIIVISET_JA_PAATTYNEET_OPINNOT")
),
MockData(
studentOid = KoskiSpecificMockOppijat.ylioppilasLukiolainen.oid,
time = "2000-01-15T20:31:32.104+03",
organizationOid = List(MockOrganisaatiot.dvv),
raw = rawAuditlog("OAUTH2_KATSOMINEN_KAIKKI_TIEDOT")
),
MockData(
studentOid = KoskiSpecificMockOppijat.ylioppilasLukiolainen.oid,
time = "2000-01-16T20:31:32.104+03",
organizationOid = List(MockOrganisaatiot.dvv),
raw = rawAuditlog("OAUTH2_KATSOMINEN_SUORITETUT_TUTKINNOT")
),
MockData(
studentOid = KoskiSpecificMockOppijat.ylioppilasLukiolainen.oid,
time = "2000-01-17T20:31:32.104+03",
organizationOid = List(MockOrganisaatiot.dvv),
raw = rawAuditlog("OAUTH2_KATSOMINEN_AKTIIVISET_JA_PAATTYNEET_OPINNOT")
),
MockData(
studentOid = KoskiSpecificMockOppijat.master.oid,
time = "2018-07-20T21:38:35.104+03",
organizationOid = List(MockOrganisaatiot.stadinAmmattiopisto),
raw = rawAuditlog("OPISKELUOIKEUS_KATSOMINEN")
),
MockData(
studentOid = KoskiSpecificMockOppijat.slave.henkilö.oid,
time = "2018-07-21T21:38:35.104+03",
organizationOid = List(MockOrganisaatiot.helsinginKaupunki),
raw = rawAuditlog("OPISKELUOIKEUS_KATSOMINEN")
),

)

private case class MockData(
Expand Down
106 changes: 73 additions & 33 deletions src/main/scala/fi/oph/koski/omaopintopolkuloki/AuditLogService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,69 @@ import fi.oph.koski.organisaatio.Opetushallitus
import fi.oph.koski.http.{HttpStatus, KoskiErrorCategory}
import fi.oph.koski.json.JsonSerializer
import fi.oph.koski.log.Logging
import fi.oph.koski.mydata.MyDataConfig
import fi.oph.koski.schema.LocalizedString
import fi.oph.koski.omaopintopolkuloki.AuditLogDynamoDB.AuditLogTableName
import fi.oph.koski.schema.Henkilö.Oid
import software.amazon.awssdk.services.dynamodb.model.{AttributeValue, QueryRequest}

import scala.collection.JavaConverters._
import scala.jdk.CollectionConverters._

class AuditLogService(app: KoskiApplication) extends Logging {
private val organisaatioRepository = app.organisaatioRepository
private val dynamoDB = AuditLogDynamoDB.buildDb(app.config)
class AuditLogService(val application: KoskiApplication) extends Logging with MyDataConfig {
private val organisaatioRepository = application.organisaatioRepository
private val dynamoDB = AuditLogDynamoDB.buildDb(application.config)

def queryLogsFromDynamo(oppijaOid: String): Either[HttpStatus, Seq[OrganisaationAuditLogit]] = {
runQuery(oppijaOid).flatMap(results => HttpStatus.foldEithers(buildLogs(results).toSeq))
def queryLogsFromDynamo(masterOppijaOid: String): Either[HttpStatus, Seq[OrganisaationAuditLogit]] = {
val kaikkiOppijanOidit = application.opintopolkuHenkilöFacade.findSlaveOids(masterOppijaOid).toSet + masterOppijaOid

val queryResult = kaikkiOppijanOidit
.toIterator
.flatMap(runQuery)

buildLogs(queryResult)
}

private def runQuery(oppijaOid: Oid): Iterator[util.Map[Oid, AttributeValue]] = {
val queryRequest = querySpec(oppijaOid).build()
val responses = dynamoDB.queryPaginator(queryRequest)
responses.items().iterator().asScala
}

private def runQuery(oppijaOid: String): Either[HttpStatus, Seq[util.Map[String, AttributeValue]]] = {
val querySpec = QueryRequest.builder
private def querySpec(oppijaOid: String) =
QueryRequest.builder
.tableName(AuditLogTableName)
.keyConditionExpression("studentOid = :oid")
.filterExpression("not contains (organizationOid, :self) and (contains (#rawEntry, :katsominen) or contains(#rawEntry, :varda_service))")
.filterExpression(
"""not contains (organizationOid, :self) and
| (contains (#rawEntry, :katsominen) or
| contains (#rawEntry, :muutoshistoria_katsominen) or
| contains (#rawEntry, :ytr_katsominen) or
| contains (#rawEntry, :oauth2_katsominen_kaikki_tiedot) or
| contains (#rawEntry, :oauth2_katsominen_suoritetut_tutkinnot) or
| contains (#rawEntry, :oauth2_katsominen_aktiiviset_ja_paattyneet_opinnot) or
| contains (#rawEntry, :suoritusjako_katsominen) or
| contains (#rawEntry, :suoritusjako_katsominen_suoritetut_tutkinnot) or
| contains (#rawEntry, :suoritusjako_katsominen_aktiiviset_ja_paattyneet_opinnot) or
| contains (#rawEntry, :varda_service))
| """.stripMargin)
.expressionAttributeNames(Map("#rawEntry" -> "raw").asJava)
.expressionAttributeValues({
val valueMap = new util.HashMap[String, AttributeValue]()
valueMap.put(":oid", AttributeValue.builder.s(oppijaOid).build)
valueMap.put(":self", AttributeValue.builder.s("self").build)
valueMap.put(":katsominen", AttributeValue.builder.s("\"OPISKELUOIKEUS_KATSOMINEN\"").build)
valueMap.put(":muutoshistoria_katsominen", AttributeValue.builder.s("\"MUUTOSHISTORIA_KATSOMINEN\"").build)
valueMap.put(":ytr_katsominen", AttributeValue.builder.s("\"YTR_OPISKELUOIKEUS_KATSOMINEN\"").build)
valueMap.put(":suoritusjako_katsominen", AttributeValue.builder.s("\"KANSALAINEN_SUORITUSJAKO_KATSOMINEN\"").build)
valueMap.put(":suoritusjako_katsominen_suoritetut_tutkinnot", AttributeValue.builder.s("\"KANSALAINEN_SUORITUSJAKO_KATSOMINEN_SUORITETUT_TUTKINNOT\"").build)
valueMap.put(":suoritusjako_katsominen_aktiiviset_ja_paattyneet_opinnot", AttributeValue.builder.s("\"KANSALAINEN_SUORITUSJAKO_KATSOMINEN_AKTIIVISET_JA_PAATTYNEET_OPINNOT\"").build)
valueMap.put(":oauth2_katsominen_kaikki_tiedot", AttributeValue.builder.s("\"OAUTH2_KATSOMINEN_KAIKKI_TIEDOT\"").build)
valueMap.put(":oauth2_katsominen_suoritetut_tutkinnot", AttributeValue.builder.s("\"OAUTH2_KATSOMINEN_SUORITETUT_TUTKINNOT\"").build)
valueMap.put(":oauth2_katsominen_aktiiviset_ja_paattyneet_opinnot", AttributeValue.builder.s("\"OAUTH2_KATSOMINEN_AKTIIVISET_JA_PAATTYNEET_OPINNOT\"").build)
valueMap.put(":varda_service", AttributeValue.builder.s("\"varda\"").build)
valueMap
})

try {
Right(dynamoDB.query(querySpec.build()).items().asScala)
} catch {
case e: Exception => {
logger.error(e)(s"AuditLogien haku epäonnistui oidille $oppijaOid")
Left(KoskiErrorCategory.internalError())
}
}
}
private def convertToAuditLogRow(item: util.Map[String, AttributeValue]): AuditlogRow = {
val organizationOid = item.asScala.view.collectFirst {
case ("organizationOid", value) if value.l() != null =>
Expand All @@ -64,20 +89,31 @@ class AuditLogService(app: KoskiApplication) extends Logging {
AuditlogRow(organizationOid, raw, time)
}

private def buildLogs(queryResults: Seq[util.Map[String, AttributeValue]]): Iterable[Either[HttpStatus, OrganisaationAuditLogit]] = {
val timestampsGroupedByListOfOidsAndServiceName = queryResults.map(item => {
val parsedRow = convertToAuditLogRow(item)
val parsedRaw = JsonSerializer.parse[AuditlogRaw](parsedRow.raw, ignoreExtras = true)
val organisaatioOidit = parsedRow.organizationOid.sorted
val timestampString = parsedRow.time
val serviceName = parsedRaw.serviceName
(organisaatioOidit, serviceName, timestampString)
}).groupBy(x => (x._1, x._2)).mapValues(_.map(_._3))

timestampsGroupedByListOfOidsAndServiceName.map { case ((orgs, serviceName), timestamps) =>
HttpStatus.foldEithers(orgs.map(toOrganisaatio))
.map(orgs => OrganisaationAuditLogit(orgs, serviceName, timestamps))
}
private def buildLogs(queryResult: Iterator[util.Map[Oid, AttributeValue]]): Either[HttpStatus, Seq[OrganisaationAuditLogit]] = {
val timestampsGrouped = queryResult
.map(item => {
val parsedRow = convertToAuditLogRow(item)
val parsedRaw = JsonSerializer.parse[AuditlogRaw](parsedRow.raw, ignoreExtras = true)
val organisaatioOidit = parsedRow.organizationOid.sorted
val timestampString = parsedRow.time
val serviceName = parsedRaw.serviceName
val isMyDataUse = parsedRaw.operation.startsWith("OAUTH2_KATSOMINEN") || parsedRow.organizationOid.headOption.exists(isMyDataOrg)
val isJakolinkkiUse = parsedRaw.operation.startsWith("KANSALAINEN_SUORITUSJAKO_KATSOMINEN")
(organisaatioOidit, serviceName, isMyDataUse, isJakolinkkiUse, timestampString)
})
.toSeq
.groupBy(x => (x._1, x._2, x._3, x._4))
.mapValues(_.map(_._5))

HttpStatus.foldEithers(
timestampsGrouped
.map {
case ((orgs, serviceName, isMyDataUse, isJakolinkkiUse), timestamps) =>
HttpStatus.foldEithers(orgs.map(toOrganisaatio))
.map(orgs => OrganisaationAuditLogit(orgs, serviceName, isMyDataUse, isJakolinkkiUse, timestamps))
}
.toSeq
)
}

private def toOrganisaatio(oid: String): Either[HttpStatus, Organisaatio] = {
Expand Down Expand Up @@ -105,16 +141,20 @@ case class AuditlogRow (
time: String
)
case class AuditlogRaw (
serviceName: String
serviceName: String,
operation: String
)

case class OrganisaationAuditLogit(
organizations: Seq[Organisaatio],
serviceName: String,
isMyDataUse: Boolean,
isJakolinkkiUse: Boolean,
timestamps: Seq[String]
)

case class Organisaatio(
oid: String,
name: LocalizedString
)

Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,27 @@ class OmaOpintoPolkuLokiServletSpec extends AnyFreeSpec with Matchers with Koski
List(MockOrganisaatiot.päiväkotiTouhula)
))
}
"Näytetään YTR_OPISKELUOIKEUS_KATSOMINEN- ja MUUTOSHISTORIA_KATSOMINEN -auditlogeja" in {
auditlogs(KoskiSpecificMockOppijat.ylioppilas).map(_.organizations.map(_.oid)) should contain theSameElementsAs(List(
List(MockOrganisaatiot.helsinginKaupunki),
List(MockOrganisaatiot.stadinAmmattiopisto)
))
}
"Näytetään KANSALAINEN_SUORITUSJAKO_KATSOMINEN_* - ja OAUTH2_KATSOMINEN_* -auditlogeja" in {
val logs = auditlogs(KoskiSpecificMockOppijat.ylioppilasLukiolainen)
logs should have length(2)
logs.foreach(_.timestamps should have length(3))
logs.map(_.organizations.map(_.oid)) should contain theSameElementsAs(List(
List(MockOrganisaatiot.helsinginKaupunki),
List(MockOrganisaatiot.dvv)
))
}
"Näytetään sivoppijaoidien auditlokit kysyttäessä pääoidilla" in {
auditlogs(KoskiSpecificMockOppijat.master).map(_.organizations.map(_.oid)) should contain theSameElementsAs(List(
List(MockOrganisaatiot.stadinAmmattiopisto),
List(MockOrganisaatiot.helsinginKaupunki)
))
}
"Data sisältää tiedon lähdepalvelusta" in {
auditlogs(KoskiSpecificMockOppijat.aikuisOpiskelija).map(_.serviceName) should contain theSameElementsAs List("koski")
}
Expand Down

0 comments on commit 20db8f3

Please sign in to comment.