Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TOR-2210 OmaData OAuth2: Näytä kansalaiskälissä myös OAuth2-rajapinnan kautta katselut #3229

Merged
merged 1 commit into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
108 changes: 75 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,33 @@ 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)
// TODO: Jakolinkkien käyttöjen palauttaminen frontille on toteutettu valmiiksi, mutta oma-opintopolku-lokin DynamoDB-parsinta skippaa näiden
// entryjen käsittelyn, minkä vuoksi tuotantoympäristöissä näitä entryjä ei vielä käytännössä DynamoDB:ssä ole.
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 +143,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
Loading