diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/auth/Realm.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/auth/Realm.java index fb7010325f5165428e8a09c3ddb4b9e19fc8bcf8..57b472396c243f369491faa3962dfe9c39b69831 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/auth/Realm.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/auth/Realm.java @@ -1,6 +1,7 @@ package at.tuwien.entities.auth; import lombok.*; +import org.hibernate.annotations.GenericGenerator; import javax.persistence.*; @@ -16,7 +17,9 @@ public class Realm { @Id @EqualsAndHashCode.Include - @Column(nullable = false) + @GeneratedValue(generator = "realm-uuid") + @GenericGenerator(name = "realm-uuid", strategy = "org.hibernate.id.UUIDGenerator") + @Column(name = "ID", nullable = false, columnDefinition = "VARCHAR(36)") private String id; @Column(nullable = false) @@ -25,7 +28,4 @@ public class Realm { @Column(nullable = false) private String name; - @Column(nullable = false) - private String sslRequired; - } diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Credential.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Credential.java new file mode 100644 index 0000000000000000000000000000000000000000..a189fb25d023b90a606762c3e19f97a78476fa8a --- /dev/null +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Credential.java @@ -0,0 +1,51 @@ +package at.tuwien.entities.user; + +import lombok.*; +import org.hibernate.annotations.GenericGenerator; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.*; + +@Data +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EntityListeners(AuditingEntityListener.class) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@Table(name = "credential") +public class Credential { + + @Id + @EqualsAndHashCode.Include + @GeneratedValue(generator = "credential-uuid") + @GenericGenerator(name = "credential-uuid", strategy = "org.hibernate.id.UUIDGenerator") + @Column(name = "ID", nullable = false, columnDefinition = "VARCHAR(36)") + private String id; + + @Column(nullable = false) + private String type; + + @Column(name = "user_id", nullable = false) + private String userId; + + @Column(nullable = false) + private Long createdDate; + + @Column(nullable = false, columnDefinition = "LONGTEXT") + private String secretData; + + @Column(nullable = false, columnDefinition = "LONGTEXT") + private String credentialData; + + @Column(nullable = false) + private Integer priority; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinColumns({ + @JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false) + }) + private User user; + +} diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java index 29f7222a7159524f446b764964ba361d3fea32d7..af7c01362072239002a8639480010b90b403932a 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java @@ -4,6 +4,7 @@ import at.tuwien.entities.container.Container; import at.tuwien.entities.database.Database; import at.tuwien.entities.identifier.Identifier; import lombok.*; +import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.Immutable; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -16,7 +17,6 @@ import java.util.List; @AllArgsConstructor @NoArgsConstructor @ToString -@Immutable @EntityListeners(AuditingEntityListener.class) @EqualsAndHashCode(onlyExplicitlyIncluded = true) @Table(name = "user_entity") @@ -24,7 +24,9 @@ public class User { @Id @EqualsAndHashCode.Include - @Column(nullable = false) + @GeneratedValue(generator = "user-uuid") + @GenericGenerator(name = "user-uuid", strategy = "org.hibernate.id.UUIDGenerator") + @Column(name = "ID", nullable = false, columnDefinition = "VARCHAR(36)") private String id; @Column(unique = true, nullable = false) @@ -45,11 +47,21 @@ public class User { @Column(nullable = false) private Boolean emailVerified; + @Column(nullable = false) + private Boolean enabled; + + @Column + private Long createdTimestamp; + @Transient @ToString.Exclude @Column(nullable = false) private String databasePassword; + @Column(nullable = false) + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "user") + private List<Credential> credentials; + @Transient @ToString.Exclude @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "owner") diff --git a/fda-ui/api/analyse/index.js b/fda-ui/api/analyse/index.js new file mode 100644 index 0000000000000000000000000000000000000000..66ad332a56dfc7b58104f77c5c8fdd206e4f25a7 --- /dev/null +++ b/fda-ui/api/analyse/index.js @@ -0,0 +1,12 @@ +const axios = require('axios/dist/browser/axios.cjs') + +export function determineDataTypes (token, filepath) { + const payload = { + filepath + } + return axios.post('/api/analyse/determinedt', payload, { + headers: { + Authorization: `Bearer ${token}` + } + }) +} diff --git a/fda-ui/api/container/index.js b/fda-ui/api/container/index.js index f5b6e59f80e395734784f08923d85adb48727ed4..594657500b1fa953ce3592333fe1a01940da9f47 100644 --- a/fda-ui/api/container/index.js +++ b/fda-ui/api/container/index.js @@ -4,6 +4,10 @@ export function listContainers (limit) { return axios.get(`/api/container?limit=${limit}`) } +export function findContainer (containerId) { + return axios.get(`/api/container${containerId}`) +} + export function createContainer (token, payload) { return axios.post('/api/container/', payload, { headers: { diff --git a/fda-ui/api/database/index.js b/fda-ui/api/database/index.js index 986311fec70fbd6e6104b87451a46cd4c594904e..dee8179bf5dbfefba9426f060ebef5664caa4e49 100644 --- a/fda-ui/api/database/index.js +++ b/fda-ui/api/database/index.js @@ -23,6 +23,17 @@ export function modifyVisibility (token, containerId, databaseId, isPublic) { }) } +export function modifyOwnership (token, containerId, databaseId, username) { + const payload = { + username + } + return axios.put(`/api/container/${containerId}/database/${databaseId}/transfer`, payload, { + headers: { + Authorization: `Bearer ${token}` + } + }) +} + export function findDatabase (token, containerId, databaseId) { return axios.get(`/api/container/${containerId}/database/${databaseId}`, { headers: { diff --git a/fda-ui/api/table/index.js b/fda-ui/api/table/index.js index d0f5aa0b6fbb21b1610d13ef6ee45e5f7daa0109..097d003972837b4d640125704831315256360bfb 100644 --- a/fda-ui/api/table/index.js +++ b/fda-ui/api/table/index.js @@ -15,3 +15,11 @@ export function createTable (token, containerId, databaseId, payload) { } }) } + +export function dataImport (token, containerId, databaseId, tableId, payload) { + return axios.post(`/api/container/${containerId}/database/${databaseId}/table/${tableId}/data/import`, payload, { + headers: { + Authorization: `Bearer ${token}` + } + }) +} diff --git a/fda-ui/api/user/index.js b/fda-ui/api/user/index.js index 8f3edc2f2c93cd476e8d5210e6c6f57fef7dfb08..670535c449675ff6a20af05e5bc89fedb65e6bac 100644 --- a/fda-ui/api/user/index.js +++ b/fda-ui/api/user/index.js @@ -51,10 +51,16 @@ export function tokenToUser (token) { export function tokenToExp (token) { const data = jwt_decode(token) + if (!data) { + return new Date() + } return new Date(data.exp * 1000) } export function tokenToRoles (token) { const data = jwt_decode(token) - return data.realm_access.roles + if (!data) { + return [] + } + return data.realm_access.roles || [] } diff --git a/fda-ui/components/DatabaseList.vue b/fda-ui/components/DatabaseList.vue index 572370fd26be5f68a538cece4bf8db087a2fbace..81e96814351acfe3f16a99e3a0a48cc0e84158d7 100644 --- a/fda-ui/components/DatabaseList.vue +++ b/fda-ui/components/DatabaseList.vue @@ -98,20 +98,20 @@ export default { }, methods: { formatOwner (container) { - if (!('database' in container)) { + if (!('database' in container) || !container.database) { return formatUser(container.creator) } return formatUser(container.database?.owner) }, formatCreators (container) { const creators = formatCreators(container) - return creators || this.formatCreator(container.creator) + return creators || this.formatUser(container.creator) }, canInit (container) { if (!this.user) { return false } - if (container.creator.sub !== this.user.sub) { + if (container.creator.username !== this.user.username) { return false } return !container.database diff --git a/fda-ui/config.js b/fda-ui/config.js index b2ea4ef41e7c38bb63c62df110c71fbfd78cb82d..e2bc44ee35ecae1e00629b55538f13239466bb1e 100644 --- a/fda-ui/config.js +++ b/fda-ui/config.js @@ -1,6 +1,6 @@ const config = {} -config.api = process.env.API || 'http://localhost:9095' +config.api = process.env.API || 'https://gateway-service:9095' config.search = process.env.SEARCH || 'http://localhost:9200' config.sandbox = process.env.SANDBOX || false config.title = process.env.TITLE || 'Database Repository' diff --git a/fda-ui/layouts/default.vue b/fda-ui/layouts/default.vue index 2f68a1ade6cbbe5baedcdf92565ed28883df4a3f..064a8436d0588c230a041f253fc5c40c68d6a707 100644 --- a/fda-ui/layouts/default.vue +++ b/fda-ui/layouts/default.vue @@ -252,7 +252,7 @@ export default { this.$router.push({ path: '/login', query: redirect ? { redirect: this.$router.currentRoute.path } : {} }) }, logout (message) { - if (message) { + if (typeof message === 'string') { this.$toast.warning(message) } this.$store.commit('SET_TOKEN', null) diff --git a/fda-ui/nuxt.config.js b/fda-ui/nuxt.config.js index 1f4565dffb49e5e3e18588df6561bf4d53e4a121..f84af6fa4507edf8b084a2ffc3ed978bea1797c7 100644 --- a/fda-ui/nuxt.config.js +++ b/fda-ui/nuxt.config.js @@ -84,7 +84,7 @@ export default { proxy: { '/api': api, '/pid': { - target: process.env.API + '/api' || 'https://localhost:9095/api', + target: api + '/api', changeOrigin: true, pathRewrite: { '^/pid': '/pid' diff --git a/fda-ui/package.json b/fda-ui/package.json index ff23e62ae94a9fff5f6ad2d24cc4092fa5e861d5..2548fe968b7e116e56a99218d5e85e775b56055c 100644 --- a/fda-ui/package.json +++ b/fda-ui/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "nuxt --port 3001", + "dev": "NODE_OPTIONS=--use-openssl-ca NODE_EXTRA_CA_CERTS=./root.crt nuxt --port 3001", "docker": "nuxt > /dev/null", "build": "nuxt build", "start": "nuxt start", diff --git a/fda-ui/pages/container/_container_id/database/_database_id/settings.vue b/fda-ui/pages/container/_container_id/database/_database_id/settings.vue index 888ba4f681117fd66f97a8df17748ec5b3998450..2c4fc09eeb7e447015d0822ee0b6ecef9e406fb4 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/settings.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/settings.vue @@ -64,6 +64,8 @@ id="owner" v-model="modifyOwner.username" :items="users" + item-text="username" + item-value="username" label="Owner" name="owner" /> </v-col> @@ -72,8 +74,8 @@ small color="warning" class="black--text" - @click="updateDatabaseVisibility"> - Modify Visibility + @click="updateDatabaseOwner"> + Modify Ownership </v-btn> </v-card-text> </v-card> @@ -91,7 +93,7 @@ <script> import DBToolbar from '@/components/DBToolbar' import EditAccess from '@/components/dialogs/EditAccess' -import { modifyVisibility } from '@/api/database' +import { modifyVisibility, modifyOwnership } from '@/api/database' export default { components: { @@ -186,6 +188,7 @@ export default { return } this.modifyVisibility.is_public = this.database.is_public + this.modifyOwner.username = this.database.owner.username } }, mounted () { @@ -194,6 +197,7 @@ export default { return } this.modifyVisibility.is_public = this.database.is_public + this.modifyOwner.username = this.database.owner.username }, methods: { closeDialog (event) { @@ -215,6 +219,17 @@ export default { } this.loading = false }, + async updateDatabaseOwner () { + try { + this.loading = true + await modifyOwnership(this.token, this.$route.params.container_id, this.$route.params.database_id, this.modifyOwner.username) + this.$toast.success('Successfully updated the database owner') + } catch (error) { + console.error('Failed to update database owner', error) + this.$toast.error('Failed to update database owner') + } + this.loading = false + }, giveAccess () { this.username = null this.editAccessDialog = true diff --git a/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue b/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue index 634c92fbed91ef8cb7434461b7c6ddaf6c8616ee..f94d97345ffaf8d03f5e829fb6a0f97cc32b1c5c 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue @@ -187,7 +187,9 @@ <script> import TableSchema from '@/components/TableSchema' import { notEmpty, isNonNegativeInteger, isResearcher } from '@/utils' -import { listTables } from '@/api/table' +import { findContainer } from '@/api/container' +import { listTables, createTable, dataImport } from '@/api/table' +import { determineDataTypes } from '@/api/analyse' export default { name: 'TableFromCSV', @@ -317,7 +319,6 @@ export default { this.file = res.data } catch (err) { console.error('Failed to upload .csv data', err) - console.debug('failed to upload .csv data, does the .csv contain a header line?') this.$toast.error('Could not upload data') } this.loading = false @@ -325,8 +326,7 @@ export default { async analyse () { this.loading = true try { - const payload = { filepath: `/tmp/${this.file.filename}` } - const res = await this.$axios.post('/api/analyse/determinedt', payload, this.config) + const res = await determineDataTypes(this.token, `/tmp/${this.file.filename}`) const { columns } = res.data console.log('data analyse result', columns) this.tableCreate.columns = Object.entries(columns) @@ -334,12 +334,8 @@ export default { return { name: key, type: val, - check_expression: null, - foreign_key: null, - references: null, null_allowed: true, primary_key: false, - unique: false, enum_values: [] } }) @@ -382,11 +378,10 @@ export default { column.unique = true }, async loadDateFormats () { - const getUrl = `/api/container/${this.$route.params.container_id}` let getResult try { this.loading = true - getResult = await this.$axios.get(getUrl, this.config) + getResult = await findContainer(this.$route.params.container_id) this.dateFormats = getResult.data.image.date_formats console.debug('retrieve image date formats', this.dateFormats) this.loading = false @@ -415,11 +410,10 @@ export default { // bail out if there is a problem with one of the columns if (!validColumns.every(Boolean)) { return } - const createUrl = `/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table` let createResult try { this.loading = true - createResult = await this.$axios.post(createUrl, this.tableCreate, this.config) + createResult = await createTable(this.token, this.$route.params.container_id, this.$route.params.database_id, this.tableCreate) this.newTableId = createResult.data.id console.debug('created table', createResult.data) } catch (err) { @@ -433,10 +427,9 @@ export default { console.error('create table failed', err) return } - const insertUrl = `/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${createResult.data.id}/data/import` let insertResult try { - insertResult = await this.$axios.post(insertUrl, this.tableImport, this.config) + insertResult = await dataImport(this.token, this.$route.params.container_id, this.$route.params.database_id, createResult.data.id, this.tableImport) console.debug('inserted table', insertResult.data) } catch (err) { this.loading = false diff --git a/fda-ui/utils/index.js b/fda-ui/utils/index.js index 3772dbb7a808a9dcf52ab2a0c0f2b12a6b18f93d..09b2314944314db688b7505166670251a709b461 100644 --- a/fda-ui/utils/index.js +++ b/fda-ui/utils/index.js @@ -54,10 +54,10 @@ function formatUser (user) { if (!user) { return null } - if (!('firstname' in user) || !('lastname' in user) || user.firstname === null || user.lastname === null) { + if (!('given_name' in user) || !('family_name' in user) || user.given_name === null || user.family_name === null) { return user?.username } - return user.firstname + ' ' + user.lastname + return user.given_name + ' ' + user.family_name } function formatDateUTC (str) { diff --git a/fda-user-service/pom.xml b/fda-user-service/pom.xml index 989dafab4ceaf1fb5bc6dbcdf3027cb8821e8b3d..8b3b45fbf1906a8e8d0261b470e5449dbed44645 100644 --- a/fda-user-service/pom.xml +++ b/fda-user-service/pom.xml @@ -51,6 +51,7 @@ <hibernate-c3po.version>5.6.3.Final</hibernate-c3po.version> <maven-report.version>3.0.0</maven-report.version> <jwt.version>4.3.0</jwt.version> + <keycloak.version>21.0.2</keycloak.version> </properties> <dependencies> @@ -97,6 +98,11 @@ <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- Authentication --> + <dependency> + <groupId>org.keycloak</groupId> + <artifactId>keycloak-common</artifactId> + <version>${keycloak.version}</version> + </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java b/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java index 485dcccce4861baed02335cc8de4e05248631ea2..0d5148892f22a65c92272cd841f559879203d929 100644 --- a/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java +++ b/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java @@ -2,9 +2,13 @@ package at.tuwien.endpoint; import at.tuwien.api.auth.SignupRequestDto; import at.tuwien.api.user.UserBriefDto; +import at.tuwien.entities.auth.Realm; +import at.tuwien.exception.RealmNotFoundException; import at.tuwien.exception.RemoteUnavailableException; +import at.tuwien.exception.UserAlreadyExistsException; import at.tuwien.exception.UserNotFoundException; import at.tuwien.mapper.UserMapper; +import at.tuwien.service.RealmService; import at.tuwien.service.UserService; import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.Operation; @@ -28,11 +32,13 @@ public class UserEndpoint { private final UserMapper userMapper; private final UserService userService; + private final RealmService realmService; @Autowired - public UserEndpoint(UserMapper userMapper, UserService userService) { + public UserEndpoint(UserMapper userMapper, UserService userService, RealmService realmService) { this.userMapper = userMapper; this.userService = userService; + this.realmService = realmService; } @GetMapping @@ -54,9 +60,11 @@ public class UserEndpoint { @Timed(value = "user.create", description = "Time needed to create a user in the metadata database") @Operation(summary = "Create a user") public ResponseEntity<UserBriefDto> create(@NotNull @Valid @RequestBody SignupRequestDto data) - throws UserNotFoundException, RemoteUnavailableException { + throws UserNotFoundException, RemoteUnavailableException, RealmNotFoundException, + UserAlreadyExistsException { log.debug("endpoint create a user, data={}", data); - final UserBriefDto dto = userMapper.userToUserBriefDto(userService.create(data)); + final Realm realm = realmService.find("dbrepo"); + final UserBriefDto dto = userMapper.userToUserBriefDto(userService.create(data, realm)); log.trace("create user resulted in dto {}", dto); return ResponseEntity.status(HttpStatus.CREATED) .body(dto); diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/RealmNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/RealmNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..1750cfb525c2947f8f13837b5e89ed7ddc46f8fd --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/RealmNotFoundException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND) +public class RealmNotFoundException extends Exception { + + public RealmNotFoundException(String msg) { + super(msg); + } + + public RealmNotFoundException(String msg, Throwable thr) { + super(msg, thr); + } + + public RealmNotFoundException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/UserAlreadyExistsException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/UserAlreadyExistsException.java new file mode 100644 index 0000000000000000000000000000000000000000..bca8199ae01a314e400ba065d367efe84dc6e972 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/UserAlreadyExistsException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.CONFLICT) +public class UserAlreadyExistsException extends Exception { + + public UserAlreadyExistsException(String message) { + super(message); + } + + public UserAlreadyExistsException(String message, Throwable thr) { + super(message, thr); + } + + public UserAlreadyExistsException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java index 308cda436322a23d4509a10e7630dea8c0775065..1313e12b3b702a085222b5b77b52fdd228fb034f 100644 --- a/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java +++ b/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java @@ -1,7 +1,5 @@ package at.tuwien.mapper; -import at.tuwien.api.auth.CreateUserDto; -import at.tuwien.api.auth.CredentialDto; import at.tuwien.api.auth.SignupRequestDto; import at.tuwien.api.user.GrantedAuthorityDto; import at.tuwien.api.user.UserBriefDto; @@ -12,7 +10,6 @@ import org.mapstruct.Mapper; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import java.util.List; @Mapper(componentModel = "spring") public interface UserMapper { @@ -25,18 +22,7 @@ public interface UserMapper { UserBriefDto userToUserBriefDto(User data); - default CreateUserDto signupRequestDtoToCreateUserDto(SignupRequestDto data) { - return CreateUserDto.builder() - .username(data.getUsername()) - .email(data.getEmail()) - .enabled(true) - .credentials(List.of(CredentialDto.builder() - .temporary(false) - .type("password") - .value(data.getPassword()) - .build())) - .build(); - } + User signupRequestDtoToUser(SignupRequestDto data); default GrantedAuthority grantedAuthorityDtoToGrantedAuthority(GrantedAuthorityDto data) { final GrantedAuthority authority = new SimpleGrantedAuthority(data.getAuthority()); diff --git a/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/CredentialRepository.java b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/CredentialRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..eb43a5af74d5863735c9f532351a4f40bb6f35aa --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/CredentialRepository.java @@ -0,0 +1,10 @@ +package at.tuwien.repository.jpa; + +import at.tuwien.entities.user.Credential; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CredentialRepository extends JpaRepository<Credential, String> { + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RealmRepository.java b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RealmRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..db0443c0a78c3c8c2d653edf15901c9a3f2e4686 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RealmRepository.java @@ -0,0 +1,14 @@ +package at.tuwien.repository.jpa; + +import at.tuwien.entities.auth.Realm; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RealmRepository extends JpaRepository<Realm, String> { + + Optional<Realm> findByName(String name); + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/RealmService.java b/fda-user-service/services/src/main/java/at/tuwien/service/RealmService.java new file mode 100644 index 0000000000000000000000000000000000000000..768b437a715f78830e3b1a8182cbc90a4a514614 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/service/RealmService.java @@ -0,0 +1,16 @@ +package at.tuwien.service; + +import at.tuwien.entities.auth.Realm; +import at.tuwien.exception.RealmNotFoundException; + +public interface RealmService { + + /** + * Finds a realm by name. + * + * @param name The realm name. + * @return The realm, if successful. + * @throws RealmNotFoundException The realm could not be found. + */ + Realm find(String name) throws RealmNotFoundException; +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java b/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java index a44429ae15846283fe9a6efca7a7cb5a6856c342..33addb5732aac74028b7b5e9ce480bed32eda19a 100644 --- a/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java +++ b/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java @@ -1,8 +1,10 @@ package at.tuwien.service; import at.tuwien.api.auth.SignupRequestDto; +import at.tuwien.entities.auth.Realm; import at.tuwien.entities.user.User; import at.tuwien.exception.RemoteUnavailableException; +import at.tuwien.exception.UserAlreadyExistsException; import at.tuwien.exception.UserNotFoundException; import java.util.List; @@ -25,22 +27,7 @@ public interface UserService { */ User findByUsername(String username) throws UserNotFoundException; - /** - * Create a user in the authentication service. - * - * @param data The user data. - * @return The user, if successful. - * @throws RemoteUnavailableException - * @throws UserNotFoundException - */ - User create(SignupRequestDto data) throws RemoteUnavailableException, UserNotFoundException; + User create(SignupRequestDto data, Realm realm) throws RemoteUnavailableException, UserNotFoundException, UserAlreadyExistsException; - /** - * Finds a user by id. - * - * @param id The id. - * @return The user. - * @throws UserNotFoundException The user was not found in the metadata database. - */ User find(String id) throws UserNotFoundException; } diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/impl/RealmServiceImpl.java b/fda-user-service/services/src/main/java/at/tuwien/service/impl/RealmServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..6c971add7fe6deed7968b00a7980e868dfad382c --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/service/impl/RealmServiceImpl.java @@ -0,0 +1,35 @@ +package at.tuwien.service.impl; + +import at.tuwien.entities.auth.Realm; +import at.tuwien.exception.RealmNotFoundException; +import at.tuwien.repository.jpa.RealmRepository; +import at.tuwien.service.RealmService; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Log4j2 +@Service +public class RealmServiceImpl implements RealmService { + + private final RealmRepository realmRepository; + + @Autowired + public RealmServiceImpl(RealmRepository realmRepository) { + this.realmRepository = realmRepository; + } + + @Override + public Realm find(String name) throws RealmNotFoundException { + final Optional<Realm> optional = realmRepository.findByName(name); + if (optional.isEmpty()) { + log.error("Failed to find realm with name '{}'", name); + throw new RealmNotFoundException("Failed to find realm"); + } + final Realm realm = optional.get(); + log.trace("found realm {}", realm); + return realm; + } +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java index ada4ce60f13e92c330d13a41be2d478e03b97eda..514460cf5642aaf48706b9fe06324e15dc96d186 100644 --- a/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java +++ b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java @@ -1,19 +1,29 @@ package at.tuwien.service.impl; -import at.tuwien.api.auth.CreateUserDto; import at.tuwien.api.auth.SignupRequestDto; -import at.tuwien.api.auth.TokenDto; +import at.tuwien.entities.auth.Realm; +import at.tuwien.entities.user.Credential; import at.tuwien.entities.user.User; import at.tuwien.exception.RemoteUnavailableException; +import at.tuwien.exception.UserAlreadyExistsException; import at.tuwien.exception.UserNotFoundException; -import at.tuwien.gateway.GatewayServiceGateway; import at.tuwien.mapper.UserMapper; +import at.tuwien.repository.jpa.CredentialRepository; import at.tuwien.repository.jpa.UserRepository; import at.tuwien.service.UserService; import lombok.extern.log4j.Log4j2; +import org.keycloak.common.util.Base64; +import org.keycloak.common.util.PaddingUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.time.Instant; import java.util.List; import java.util.Optional; @@ -21,16 +31,23 @@ import java.util.Optional; @Service public class UserServiceImpl implements UserService { + + private static final String ID = "pbkdf2-sha256"; + private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256"; + private static final int DEFAULT_ITERATIONS = 27500; + private static final Integer DERIVED_KEY_SIZE = 256; + private static final Integer MAX_PADDING_LENGTH = 14; + private final UserMapper userMapper; private final UserRepository userRepository; - private final GatewayServiceGateway authenticationServiceGateway; + private final CredentialRepository credentialRepository; @Autowired public UserServiceImpl(UserMapper userMapper, UserRepository userRepository, - GatewayServiceGateway authenticationServiceGateway) { + CredentialRepository credentialRepository) { this.userMapper = userMapper; this.userRepository = userRepository; - this.authenticationServiceGateway = authenticationServiceGateway; + this.credentialRepository = credentialRepository; } @Override @@ -49,17 +66,41 @@ public class UserServiceImpl implements UserService { } @Override - public User create(SignupRequestDto data) throws RemoteUnavailableException, UserNotFoundException { - final TokenDto tokenDto = authenticationServiceGateway.getToken(); - final CreateUserDto userDto = userMapper.signupRequestDtoToCreateUserDto(data); - authenticationServiceGateway.createUser(tokenDto.getAccessToken(), userDto); + public User create(SignupRequestDto data, Realm realm) throws RemoteUnavailableException, UserNotFoundException, + UserAlreadyExistsException { + /* check */ final Optional<User> optional = userRepository.findByUsername(data.getUsername()); - if (optional.isEmpty()) { - /* should never occur */ - throw new UserNotFoundException("User not found with username '" + data.getUsername() + "'"); + if (optional.isPresent()) { + log.error("User with username {} already exists", data.getUsername()); + throw new UserAlreadyExistsException("User with username " + data.getUsername() + " already exists"); } - final User user = optional.get(); + /* create secret */ + + /* save */ + final User tmp = userMapper.signupRequestDtoToUser(data); + tmp.setEmailVerified(false); + tmp.setEnabled(true); + tmp.setRealmId(realm.getId()); + tmp.setCreatedTimestamp(Instant.now().toEpochMilli()); + final byte[] salt = getSalt(); + final StringBuilder secretData = new StringBuilder("{\"value\":\"") + .append(encodedCredential(data.getPassword(), DEFAULT_ITERATIONS, salt, DERIVED_KEY_SIZE)) + .append("\",\"salt\":\"") + .append(Base64.encodeBytes(salt)) + .append("\",\"additionalParameters\":{}}"); + final Credential entity = Credential.builder() + .createdDate(Instant.now().toEpochMilli()) + .secretData(secretData.toString()) + .type("password") + .priority(10) + .credentialData("{\"hashIterations\":" + DEFAULT_ITERATIONS + ",\"algorithm\":\"" + ID + "\",\"additionalParameters\":{}}") + .build(); + final User user = userRepository.save(tmp); + entity.setUserId(user.getId()); + final Credential credential = credentialRepository.save(entity); + user.setCredentials(List.of(credential)); log.info("Created user with id {}", user.getId()); + log.debug("created user {}", user); return user; } @@ -73,4 +114,32 @@ public class UserServiceImpl implements UserService { return optional.get(); } + private String encodedCredential(String rawPassword, int iterations, byte[] salt, int derivedKeySize) { + final String rawPasswordWithPadding = PaddingUtils.padding(rawPassword, MAX_PADDING_LENGTH); + log.trace("padding: {}", rawPasswordWithPadding); + final KeySpec spec = new PBEKeySpec(rawPasswordWithPadding.toCharArray(), salt, iterations, derivedKeySize); + try { + byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded(); + return Base64.encodeBytes(key); + } catch (InvalidKeySpecException e) { + throw new RuntimeException("Credential could not be encoded", e); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private byte[] getSalt() { + byte[] buffer = new byte[16]; + final SecureRandom secureRandom = new SecureRandom(); + secureRandom.nextBytes(buffer); + return buffer; + } + + private SecretKeyFactory getSecretKeyFactory() { + try { + return SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(PBKDF2_ALGORITHM + " algorithm not found", e); + } + } }