diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 8afb2f186c..1f710f368d 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -208,6 +208,7 @@ 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" @@ -215,6 +216,7 @@ mydata = { purpose = "" membercodes = ["2769790-2"] subsystemcodes = ["koski"] + orgOid = "1.2.246.562.10.46399742280" }, ] callbackURLs = [ diff --git a/src/main/scala/fi/oph/koski/mydata/MyDataConfig.scala b/src/main/scala/fi/oph/koski/mydata/MyDataConfig.scala index 661e222745..776c01e434 100644 --- a/src/main/scala/fi/oph/koski/mydata/MyDataConfig.scala +++ b/src/main/scala/fi/oph/koski/mydata/MyDataConfig.scala @@ -40,4 +40,9 @@ trait MyDataConfig extends Logging { ) } + def isMyDataOrg(orgOid: String): Boolean = { + conf.getConfigList("members").asScala.exists(member => + member.getString("orgOid") == orgOid + ) + } } diff --git a/src/main/scala/fi/oph/koski/omaopintopolkuloki/AuditLogMockData.scala b/src/main/scala/fi/oph/koski/omaopintopolkuloki/AuditLogMockData.scala index 25acdc50c6..fe183dca25 100644 --- a/src/main/scala/fi/oph/koski/omaopintopolkuloki/AuditLogMockData.scala +++ b/src/main/scala/fi/oph/koski/omaopintopolkuloki/AuditLogMockData.scala @@ -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", @@ -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( diff --git a/src/main/scala/fi/oph/koski/omaopintopolkuloki/AuditLogService.scala b/src/main/scala/fi/oph/koski/omaopintopolkuloki/AuditLogService.scala index 8f68d8945c..c3d3928aed 100644 --- a/src/main/scala/fi/oph/koski/omaopintopolkuloki/AuditLogService.scala +++ b/src/main/scala/fi/oph/koski/omaopintopolkuloki/AuditLogService.scala @@ -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 => @@ -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] = { @@ -105,12 +141,15 @@ 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] ) @@ -118,3 +157,4 @@ case class Organisaatio( oid: String, name: LocalizedString ) + diff --git a/src/test/scala/fi/oph/koski/omaopintopolkuloki/OmaOpintoPolkuLokiServletSpec.scala b/src/test/scala/fi/oph/koski/omaopintopolkuloki/OmaOpintoPolkuLokiServletSpec.scala index 0f4a8bd594..ef37503b25 100644 --- a/src/test/scala/fi/oph/koski/omaopintopolkuloki/OmaOpintoPolkuLokiServletSpec.scala +++ b/src/test/scala/fi/oph/koski/omaopintopolkuloki/OmaOpintoPolkuLokiServletSpec.scala @@ -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") }