diff --git a/build.gradle b/build.gradle index 3aa234a..3df25a3 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' implementation 'io.netty:netty-resolver-dns-native-macos:4.1.72.Final:osx-aarch_64' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + runtimeOnly 'org.postgresql:postgresql' + implementation 'org.flywaydb:flyway-core' + implementation 'no.fintlabs:fint-kafka:4.0.1' implementation 'no.fintlabs:fint-flyt-resource-server:2.1.0-rc-3' diff --git a/kustomize/base/flais.yaml b/kustomize/base/flais.yaml index 12d26c1..f16121d 100644 --- a/kustomize/base/flais.yaml +++ b/kustomize/base/flais.yaml @@ -21,6 +21,8 @@ spec: acls: - permission: admin topic: 'no-permission' + database: + database: fint-flyt url: hostname: flyt.vigoiks.no basePath: path diff --git a/src/main/java/no/fintlabs/AuthorizationController.java b/src/main/java/no/fintlabs/AuthorizationController.java deleted file mode 100644 index c848a6a..0000000 --- a/src/main/java/no/fintlabs/AuthorizationController.java +++ /dev/null @@ -1,42 +0,0 @@ -package no.fintlabs; - -import lombok.extern.slf4j.Slf4j; -import no.fintlabs.models.user.AuthorizedUser; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.context.ReactiveSecurityContextHolder; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Mono; - -import static no.fintlabs.resourceserver.UrlPaths.INTERNAL_API; - -@RestController -@RequestMapping(INTERNAL_API + "/authorization") -@Slf4j -public class AuthorizationController { - - public AuthorizationController() { - } - - @GetMapping("check-authorized") - public ResponseEntity checkAuthorization() { - return ResponseEntity.ok("User authorized"); - } - - @GetMapping("user") - public Mono> checkUser() { - return ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) - .flatMap(authentication -> { - if (authentication != null && authentication.getAuthorities().stream() - .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_ADMIN"))) { - return Mono.just(ResponseEntity.ok(AuthorizedUser.builder().admin(true).build())); - } else { - return Mono.just(ResponseEntity.ok(AuthorizedUser.builder().admin(false).build())); - } - }); - } - -} diff --git a/src/main/java/no/fintlabs/authorization/AuthorizationUtil.java b/src/main/java/no/fintlabs/authorization/AuthorizationUtil.java new file mode 100644 index 0000000..41938c3 --- /dev/null +++ b/src/main/java/no/fintlabs/authorization/AuthorizationUtil.java @@ -0,0 +1,11 @@ +package no.fintlabs.authorization; + +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Service; + +@Service +public class AuthorizationUtil { + public String getObjectIdentifierFromToken(JwtAuthenticationToken jwtAuthenticationToken) { + return jwtAuthenticationToken.getTokenAttributes().get("objectidentifier").toString(); + } +} diff --git a/src/main/java/no/fintlabs/models/user/AuthorizedUser.java b/src/main/java/no/fintlabs/authorization/adminuser/AdminUser.java similarity index 57% rename from src/main/java/no/fintlabs/models/user/AuthorizedUser.java rename to src/main/java/no/fintlabs/authorization/adminuser/AdminUser.java index 7602ebe..bc09ef4 100644 --- a/src/main/java/no/fintlabs/models/user/AuthorizedUser.java +++ b/src/main/java/no/fintlabs/authorization/adminuser/AdminUser.java @@ -1,10 +1,10 @@ -package no.fintlabs.models.user; +package no.fintlabs.authorization.adminuser; import lombok.Builder; import lombok.Getter; @Builder @Getter -public class AuthorizedUser { +public class AdminUser { private boolean admin; } diff --git a/src/main/java/no/fintlabs/authorization/adminuser/AdminUserController.java b/src/main/java/no/fintlabs/authorization/adminuser/AdminUserController.java new file mode 100644 index 0000000..3481c13 --- /dev/null +++ b/src/main/java/no/fintlabs/authorization/adminuser/AdminUserController.java @@ -0,0 +1,111 @@ +package no.fintlabs.authorization.adminuser; + +import no.fintlabs.authorization.user.UserPermission; +import no.fintlabs.authorization.user.UserPermissionDto; +import no.fintlabs.authorization.user.UserPermissionRepository; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.util.List; +import java.util.Optional; + +import static no.fintlabs.resourceserver.UrlPaths.INTERNAL_API; + +@RestController +@RequestMapping(INTERNAL_API + "/authorization/adminuser") +public class AdminUserController { + + private final UserPermissionRepository userPermissionRepository; + + public AdminUserController( + UserPermissionRepository userPermissionRepository) { + this.userPermissionRepository = userPermissionRepository; + } + + + @GetMapping("check-is-admin") + public Mono> checkAdminUser( + @AuthenticationPrincipal Mono authenticationMono + ) { + return isAdmin(authenticationMono) + .map(isAdmin -> ResponseEntity.ok(AdminUser.builder().admin(isAdmin).build())); + } + + @GetMapping("userpermissions") + public Mono>> getUserPermissions( + @AuthenticationPrincipal Mono authenticationMono + ) { + return isAdmin(authenticationMono) + .flatMap(isAdmin -> { + if (isAdmin) { + return Mono.fromCallable(userPermissionRepository::findAll) + .subscribeOn(Schedulers.boundedElastic()) + .map(userPermissions -> { + List userPermissionDtos = new java.util.ArrayList<>(List.of()); + userPermissions.forEach(userPermission -> userPermissionDtos.add(buildUserPermissionDto(userPermission))); + return ResponseEntity.ok().body(userPermissionDtos); + }); + } else { + return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).build()); + } + }); + } + + @PostMapping("userpermissions") + public Mono>> setUserPermissions( + @RequestBody List userPermissionDtos, + @AuthenticationPrincipal Mono authenticationMono + ) { + return isAdmin(authenticationMono) + .flatMap(isAdmin -> { + if (isAdmin) { + return Flux.fromIterable(userPermissionDtos) + .flatMap(userPermissionDto -> Mono.fromCallable(() -> { + Optional userPermissionOptional = userPermissionRepository + .findByObjectIdentifier(userPermissionDto.getObjectIdentifier()); + if (userPermissionOptional.isPresent()) { + UserPermission userPermission = userPermissionOptional.get(); + userPermission.setSourceApplicationIds(userPermissionDto + .getSourceApplicationIds()); + userPermissionRepository.save(userPermission); + + return buildUserPermissionDto(userPermission); + } else { + UserPermission newUserPermission = UserPermission.builder() + .objectIdentifier(userPermissionDto.getObjectIdentifier()) + .sourceApplicationIds(userPermissionDto.getSourceApplicationIds()) + .build(); + userPermissionRepository.save(newUserPermission); + + return buildUserPermissionDto(newUserPermission); + } + }).subscribeOn(Schedulers.boundedElastic())) + .collectList() + .map(ResponseEntity::ok); + } else { + return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).build()); + } + }); + } + + private UserPermissionDto buildUserPermissionDto(UserPermission userPermission) { + return UserPermissionDto + .builder() + .objectIdentifier(userPermission.getObjectIdentifier()) + .sourceApplicationIds(userPermission.getSourceApplicationIds()) + .build(); + } + + private Mono isAdmin(Mono authenticationMono) { + return authenticationMono + .map(authentication -> authentication != null && authentication.getAuthorities().stream() + .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_ADMIN"))); + } + +} diff --git a/src/main/java/no/fintlabs/authorization/user/UserPermission.java b/src/main/java/no/fintlabs/authorization/user/UserPermission.java new file mode 100644 index 0000000..ca3eaeb --- /dev/null +++ b/src/main/java/no/fintlabs/authorization/user/UserPermission.java @@ -0,0 +1,32 @@ +package no.fintlabs.authorization.user; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.*; + +import javax.persistence.*; +import java.util.List; + +@Getter +@Builder +@Entity +@NoArgsConstructor +@AllArgsConstructor +public class UserPermission { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonIgnore + @Setter(AccessLevel.NONE) + private long id; + @Setter + @Column(unique = true, nullable = false) + private String objectIdentifier; + @ElementCollection + @CollectionTable( + name = "user_permission_source_application_ids", + joinColumns = @JoinColumn(name = "user_permission_id"), + uniqueConstraints = @UniqueConstraint(columnNames = {"user_permission_id", "source_application_ids"}) + ) + @Setter + @Column(name = "source_application_ids") + private List sourceApplicationIds; +} diff --git a/src/main/java/no/fintlabs/authorization/user/UserPermissionController.java b/src/main/java/no/fintlabs/authorization/user/UserPermissionController.java new file mode 100644 index 0000000..6dc5fb2 --- /dev/null +++ b/src/main/java/no/fintlabs/authorization/user/UserPermissionController.java @@ -0,0 +1,57 @@ +package no.fintlabs.authorization.user; + +import lombok.extern.slf4j.Slf4j; +import no.fintlabs.authorization.AuthorizationUtil; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import static no.fintlabs.resourceserver.UrlPaths.INTERNAL_API; + +@Slf4j +@RequestMapping(INTERNAL_API + "/authorization/user") +@RestController +public class UserPermissionController { + + private final UserPermissionRepository userPermissionRepository; + private final AuthorizationUtil authorizationUtil; + + public UserPermissionController( + UserPermissionRepository userPermissionRepository, + AuthorizationUtil authorizationUtil + ) { + this.userPermissionRepository = userPermissionRepository; + this.authorizationUtil = authorizationUtil; + } + + @GetMapping("check-authorized") + public ResponseEntity checkAuthorization() { + return ResponseEntity.ok("User authorized"); + } + + @GetMapping("permission") + public Mono> getSourceApplications( + @AuthenticationPrincipal Mono authenticationMono + ) { + return authenticationMono + .map(authentication -> authorizationUtil + .getObjectIdentifierFromToken((JwtAuthenticationToken) authentication)) + .publishOn(Schedulers.boundedElastic()) + .map(userPermissionRepository::findByObjectIdentifier) + .map(optionalUserPermission -> optionalUserPermission.map(userPermission -> ResponseEntity.ok( + UserPermissionDto + .builder() + .objectIdentifier(userPermission.getObjectIdentifier()) + .sourceApplicationIds(userPermission.getSourceApplicationIds()) + .build() + )).orElseGet(() -> ResponseEntity.notFound().build()) + ); + } + +} diff --git a/src/main/java/no/fintlabs/authorization/user/UserPermissionDto.java b/src/main/java/no/fintlabs/authorization/user/UserPermissionDto.java new file mode 100644 index 0000000..2e8fe8c --- /dev/null +++ b/src/main/java/no/fintlabs/authorization/user/UserPermissionDto.java @@ -0,0 +1,21 @@ +package no.fintlabs.authorization.user; + +import com.sun.istack.NotNull; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +@Getter +@Builder +@EqualsAndHashCode +@Jacksonized +public class UserPermissionDto { + @NotNull + private String objectIdentifier; + private String email; + @NotNull + private List sourceApplicationIds; +} diff --git a/src/main/java/no/fintlabs/authorization/user/UserPermissionRepository.java b/src/main/java/no/fintlabs/authorization/user/UserPermissionRepository.java new file mode 100644 index 0000000..e844733 --- /dev/null +++ b/src/main/java/no/fintlabs/authorization/user/UserPermissionRepository.java @@ -0,0 +1,13 @@ +package no.fintlabs.authorization.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserPermissionRepository extends JpaRepository { + + Optional findByObjectIdentifier(String sub); + +} diff --git a/src/main/resources/application-flyt-postgres.yaml b/src/main/resources/application-flyt-postgres.yaml new file mode 100644 index 0000000..930e06b --- /dev/null +++ b/src/main/resources/application-flyt-postgres.yaml @@ -0,0 +1,23 @@ +spring: + jpa: + properties: + hibernate: + jdbc: + time_zone: UTC + lob: + non_contextual_creation: true + dialect: org.hibernate.dialect.PostgreSQLDialect + enable_lazy_load_no_trans: true + hibernate: + ddl-auto: none + datasource: + driver-class-name: org.postgresql.Driver + username: ${fint.database.username} + hikari: + schema: ${fint.database.username} + url: ${fint.database.url} + password: ${fint.database.password} + + flyway: + locations: classpath:db/migration/ + lock-retry-count: 300 \ No newline at end of file diff --git a/src/main/resources/application-local-staging.yaml b/src/main/resources/application-local-staging.yaml index cbd58a2..d1bcec0 100644 --- a/src/main/resources/application-local-staging.yaml +++ b/src/main/resources/application-local-staging.yaml @@ -13,5 +13,11 @@ fint: spring: kafka: bootstrap-servers: localhost:9092 + datasource: + hikari: + schema: fintlabs_no + url: jdbc:postgresql://localhost:5435/fint-flyt-authorization-service + username: postgres + password: password server: port: 8086 \ No newline at end of file diff --git a/src/main/resources/application-schema-generation.yaml b/src/main/resources/application-schema-generation.yaml index 92777b9..9e000b3 100644 --- a/src/main/resources/application-schema-generation.yaml +++ b/src/main/resources/application-schema-generation.yaml @@ -1,6 +1,4 @@ spring: - profiles: - include: local-staging jpa: properties: javax: @@ -9,4 +7,4 @@ spring: scripts: action: create create-target: __init.sql - create-source: metadata + create-source: metadata \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 800e8da..4cfb947 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -5,4 +5,5 @@ spring: include: - flyt-kafka - flyt-logging + - flyt-postgres - flyt-resource-server \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 0000000..ed6d580 --- /dev/null +++ b/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,17 @@ +create table user_permission_source_application_ids +( + user_permission_id int8 not null, + source_application_ids int4 +); +create table user_permission +( + id bigserial not null, + object_identifier varchar(255) not null, + primary key (id) +); +alter table user_permission_source_application_ids + add constraint UK5w20jnmqeej62rhqsgyjahown unique (user_permission_id, source_application_ids); +alter table user_permission + add constraint UK_3j9pvu47o30sbeqolxqgjsnj unique (object_identifier); +alter table user_permission_source_application_ids + add constraint FKjpup6xop1xa25bcs8333uhsm8 foreign key (user_permission_id) references user_permission; diff --git a/start-postgres b/start-postgres new file mode 100755 index 0000000..ef83132 --- /dev/null +++ b/start-postgres @@ -0,0 +1,3 @@ +#!/bin/bash + +docker run --rm --name fint-flyt-authorization-service-postgres -p 5435:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_DB=fint-flyt-authorization-service -d postgres \ No newline at end of file