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

1790: Verein360 endpoint protection #1811

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
28 changes: 28 additions & 0 deletions administration/src/assets/verein360.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 27 additions & 4 deletions administration/src/bp-modules/applications/ApplicationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import formatDateWithTimezone from '../../util/formatDate'
import getApiBaseUrl from '../../util/getApiBaseUrl'
import { useAppToaster } from '../AppToaster'
import { Application } from './ApplicationsOverview'
import JsonFieldView, { JsonField, findValue } from './JsonFieldView'
import JuleicaVerificationQuickIndicator from './JuleicaVerificationQuickIndicator'
import JsonFieldView, { GeneralJsonField, JsonField, findValue } from './JsonFieldView'
import NoteDialogController from './NoteDialogController'
import PreVerifiedQuickIndicator, { PreVerifiedQuickIndicatorType } from './PreVerifiedQuickIndicator'
import VerificationsQuickIndicator from './VerificationsQuickIndicator'
import VerificationsView from './VerificationsView'

Expand Down Expand Up @@ -58,6 +58,7 @@ export const CollapseIcon = styled(Icon)`
align-self: center;
padding: 2px;
${printAwareCss};

:hover {
cursor: pointer;
color: ${Colors.GRAY1};
Expand Down Expand Up @@ -113,11 +114,33 @@ const RightElement = ({ jsonField, application }: RightElementProps): ReactEleme
return !!blueCardJuleicaEntitlement
}

const isPreVerifiedByOrganization = (): boolean => {
const applicationDetails = findValue(jsonField, 'applicationDetails', 'Array') ?? jsonField
const workAtOrganizationsEntitlement =
findValue(applicationDetails, 'blueCardWorkAtOrganizationsEntitlement', 'Array')?.value ?? []

const isAlreadyVerified = workAtOrganizationsEntitlement.some(
(entitlement: GeneralJsonField) =>
Array.isArray(entitlement.value) &&
entitlement.value.some(
(organizationField: GeneralJsonField) =>
organizationField.name === 'isAlreadyVerified' && organizationField.value === true
)
)
return isAlreadyVerified
}

const isPreVerified = isJuleicaEntitlementType() || isPreVerifiedByOrganization()

return (
<RightElementContainer>
{!!application.note && application.note.trim() && <Icon icon='annotation' intent='none' />}
{isJuleicaEntitlementType() ? (
<JuleicaVerificationQuickIndicator />
{isPreVerified ? (
<PreVerifiedQuickIndicator
type={
isJuleicaEntitlementType() ? PreVerifiedQuickIndicatorType.Juleica : PreVerifiedQuickIndicatorType.Verein360
}
/>
) : (
<VerificationsQuickIndicator verifications={application.verifications} />
)}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Tooltip } from '@blueprintjs/core'
import React, { memo } from 'react'

import JuleicaLogo from '../../assets/juleica.svg'
import Verein360Logo from '../../assets/verein360.svg'
import { UnFocusedDiv } from './VerificationsQuickIndicator'
import { Indicator, VerificationStatus } from './VerificationsView'

export enum PreVerifiedQuickIndicatorType {
Juleica,
Verein360,
}

const PreVerifiedQuickIndicator = memo(({ type }: { type: PreVerifiedQuickIndicatorType }) => {
const logo = type === PreVerifiedQuickIndicatorType.Juleica ? JuleicaLogo : Verein360Logo
return (
<Tooltip
content={
<div>
<b>Bestätigung(en) durch Organisationen:</b>
<br />
Bestätigung ist nicht erforderlich
</div>
}>
<UnFocusedDiv>
<Indicator status={VerificationStatus.Verified} />
<img src={logo} alt={type.toString()} height='100%' />
</UnFocusedDiv>
</Tooltip>
)
})

export default memo(PreVerifiedQuickIndicator)
1 change: 1 addition & 0 deletions administration/src/util/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"responsibility": "Tätigkeit",
"amountOfWork": "Arbeitsstunden pro Woche (Durchschnitt)",
"workSinceDate": "Tätig seit",
"isAlreadyVerified": "Die Angaben wurden bereits durch den zuständigen Verein bei Beantragung verifiziert",
f1sh1918 marked this conversation as resolved.
Show resolved Hide resolved
"payment": "Für diese ehrenamtliche Tätigkeit wurde eine Aufwandsentschädigung gewährt, die über den jährlichen Freibetrag hinaus geht (840 Euro Ehrenamtspauschale bzw. 3000 Euro Übungsleiterpauschale):",
"certificate": "Tätigkeitsnachweis",
"hasAcceptedPrivacyPolicy": "Ich erkläre mich damit einverstanden, dass meine Daten zum Zwecke der Antragsverarbeitung gespeichert werden und akzeptiere die Datenschutzerklärung",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ class EakApplicationMutationService {

if (isPreVerified) {
applicationHandler.setApplicationVerificationToPreVerifiedNow(verificationEntities)
} else {
applicationHandler.sendApplicationMails(applicationEntity, verificationEntities, dataFetcherResultBuilder)
}

applicationHandler.sendApplicationMails(applicationEntity, verificationEntities, dataFetcherResultBuilder)
return dataFetcherResultBuilder.data(true).build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import app.ehrenamtskarte.backend.application.database.ApplicationVerificationEn
import app.ehrenamtskarte.backend.application.database.ApplicationVerificationExternalSource
import app.ehrenamtskarte.backend.application.database.repos.ApplicationRepository
import app.ehrenamtskarte.backend.application.webservice.schema.create.Application
import app.ehrenamtskarte.backend.application.webservice.schema.create.ApplicationType
import app.ehrenamtskarte.backend.application.webservice.schema.create.BavariaCardType
import app.ehrenamtskarte.backend.application.webservice.schema.create.BlueCardEntitlementType
import app.ehrenamtskarte.backend.auth.database.ApiTokenType
import app.ehrenamtskarte.backend.auth.webservice.TokenAuthenticator
import app.ehrenamtskarte.backend.common.webservice.GraphQLContext
import app.ehrenamtskarte.backend.exception.service.UnauthorizedException
import app.ehrenamtskarte.backend.exception.webservice.exceptions.InvalidFileSizeException
import app.ehrenamtskarte.backend.exception.webservice.exceptions.InvalidFileTypeException
import app.ehrenamtskarte.backend.exception.webservice.exceptions.MailNotSentException
Expand Down Expand Up @@ -93,13 +97,56 @@ class ApplicationHandler(
val isAlreadyVerifiedList =
application.applicationDetails.blueCardEntitlement?.workAtOrganizationsEntitlement?.list?.map { it.isAlreadyVerified }
?: emptyList()
return when {
val allAlreadyVerifiedWithToken = when {
isAlreadyVerifiedList.all { it == false || it == null } -> false
isAlreadyVerifiedList.all { it == true } -> {
throw UnauthorizedException()
TokenAuthenticator.authenticate(context.request, ApiTokenType.VERIFIED_APPLICATION)
true
}

else -> throw BadRequestResponse("isAlreadyVerified must be the same for all entries")
}
if (!allAlreadyVerifiedWithToken) return false
validateAllAttributesForPreVerifiedApplication()
return true
}

private fun validateAllAttributesForPreVerifiedApplication() {
try {
val applicationDetails = application.applicationDetails

require(applicationDetails.applicationType == ApplicationType.FIRST_APPLICATION) {
"Application type must be FIRST_APPLICATION if application is already verified"
}
require(applicationDetails.cardType == BavariaCardType.BLUE) {
"Card type must be BLUE if application is already verified"
}
require(applicationDetails.wantsDigitalCard) {
"Digital card must be true if application is already verified"
}
require(!applicationDetails.wantsPhysicalCard) {
"Physical card must be false if application is already verified"
}
val blueCardEntitlement = applicationDetails.blueCardEntitlement
?: throw IllegalArgumentException("Blue card entitlement must be set if application is already verified")

val workAtOrganizationsEntitlement = blueCardEntitlement.workAtOrganizationsEntitlement
?: throw IllegalArgumentException("Work at organizations entitlement must be set if application is already verified")

require(blueCardEntitlement.entitlementType == BlueCardEntitlementType.WORK_AT_ORGANIZATIONS) {
"Entitlement type must be WORK_AT_ORGANIZATIONS if application is already verified"
}

val organizations = workAtOrganizationsEntitlement.list
require(!organizations.isNullOrEmpty()) {
"Work at organizations list cannot be empty if application is already verified"
}
require(organizations.all { it.organization.category.shortText == "Sport" }) {
"All organizations must be of category Sport if application is already verified"
}
} catch (e: IllegalArgumentException) {
throw BadRequestResponse(e.message!!)
}
}

fun setApplicationVerificationToPreVerifiedNow(verificationEntities: List<ApplicationVerificationEntity>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package app.ehrenamtskarte.backend.auth.webservice

import app.ehrenamtskarte.backend.auth.database.ApiTokenEntity
import app.ehrenamtskarte.backend.auth.database.ApiTokenType
import app.ehrenamtskarte.backend.auth.database.PasswordCrypto
import app.ehrenamtskarte.backend.auth.database.repos.ApiTokensRepository
import app.ehrenamtskarte.backend.exception.service.ForbiddenException
import app.ehrenamtskarte.backend.exception.service.UnauthorizedException
import io.javalin.http.Context
import jakarta.servlet.http.HttpServletRequest
import org.jetbrains.exposed.sql.transactions.transaction
import java.time.LocalDate

class TokenAuthenticator {
companion object TokenAuthenticator {
private fun authenticateToken(header: String?, neededType: ApiTokenType): ApiTokenEntity {
val authHeader = header?.takeIf { it.startsWith("Bearer ") }
?: throw UnauthorizedException()

val tokenHash = PasswordCrypto.hashWithSHA256(authHeader.substring(7).toByteArray())

return transaction {
ApiTokensRepository.findByTokenHash(tokenHash)
?.takeIf { it.expirationDate > LocalDate.now() && it.type == neededType }
?: throw ForbiddenException()
}
}

fun authenticate(context: Context, neededType: ApiTokenType): ApiTokenEntity =
authenticateToken(context.header("Authorization"), neededType)

fun authenticate(request: HttpServletRequest, neededType: ApiTokenType): ApiTokenEntity =
authenticateToken(request.getHeader("Authorization"), neededType)
}
}
Loading