diff --git a/fda-container-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-container-service/services/src/main/java/at/tuwien/mapper/UserMapper.java index e5ebe0bde07b297692218a9f953b349e27b7f71a..cfcab8f9975751ae5a3e50dec28285a9f2bd0ddf 100644 --- a/fda-container-service/services/src/main/java/at/tuwien/mapper/UserMapper.java +++ b/fda-container-service/services/src/main/java/at/tuwien/mapper/UserMapper.java @@ -9,7 +9,6 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.Arrays; -import java.util.List; import java.util.stream.Collectors; @Mapper(componentModel = "spring") diff --git a/fda-database-service/Dockerfile b/fda-database-service/Dockerfile index 859b9f980aaf694e3c98f956b53f07378ec25596..d57c8704f0359f1457593b693fa23cb125929038 100644 --- a/fda-database-service/Dockerfile +++ b/fda-database-service/Dockerfile @@ -31,6 +31,8 @@ ENV SEARCH_USERNAME=elastic ENV SEARCH_PASSWORD=elastic ENV GATEWAY_ENDPOINT=http://gateway-service:9095 ENV LOG_LEVEL=debug +ENV CLIENT_SECRET=client-secret +ENV CLIENT_ID=dbrepo-client COPY ./service_ready /usr/bin RUN chmod +x /usr/bin/service_ready diff --git a/fda-database-service/rest-service/src/main/resources/application-docker.yml b/fda-database-service/rest-service/src/main/resources/application-docker.yml index fec1d1fc8ad15cdebd436637a5415d3545b2e721..10adc604b2b9683de034066225b3898cd591c572 100644 --- a/fda-database-service/rest-service/src/main/resources/application-docker.yml +++ b/fda-database-service/rest-service/src/main/resources/application-docker.yml @@ -43,4 +43,6 @@ fda: username: elastic password: "${ELASTIC_PASSWORD}" ready.path: /ready + client_secret: "${CLIENT_SECRET}" + client_id: "${CLIENT_ID}" gateway.endpoint: "${GATEWAY_ENDPOINT}" \ No newline at end of file diff --git a/fda-database-service/rest-service/src/main/resources/application-local.yml b/fda-database-service/rest-service/src/main/resources/application-local.yml index f56f7a82310388e74679ea7fb0c5e12dcea89175..5ace5e75c24a6d2d3a311909a7671f332b47e4d0 100644 --- a/fda-database-service/rest-service/src/main/resources/application-local.yml +++ b/fda-database-service/rest-service/src/main/resources/application-local.yml @@ -39,8 +39,10 @@ eureka: client.serviceUrl.defaultZone: http://localhost:9090/eureka/ fda: elastic: - endpoint: search-service:9200 + endpoint: localhost:9200 username: elastic password: elastic ready.path: ./ready + client_secret: Gp9IALXWsfftK8ek1J6jNT9hNfWV5U5c + client_id: dbrepo-client gateway.endpoint: http://localhost:9095 \ No newline at end of file diff --git a/fda-database-service/rest-service/src/main/resources/application.yml b/fda-database-service/rest-service/src/main/resources/application.yml index a27d1eda9f4b9936b6b5f5723cdc44583c59e968..9ba93e20134731a4b04860faa741878f8d0a733e 100644 --- a/fda-database-service/rest-service/src/main/resources/application.yml +++ b/fda-database-service/rest-service/src/main/resources/application.yml @@ -43,4 +43,6 @@ fda: username: elastic password: "${ELASTIC_PASSWORD}" ready.path: /ready + client_secret: "${CLIENT_SECRET}" + client_id: "${CLIENT_ID}" gateway.endpoint: http://gateway-service:9095 \ No newline at end of file diff --git a/fda-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/fda-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java index c728dbbddd5d79248f5feedf42a3097fb69034e5..ce5a3eb952065f2ddcfd7f36e35819d909530f61 100644 --- a/fda-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java +++ b/fda-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java @@ -15,6 +15,13 @@ public class GatewayConfig { @Value("${fda.gateway.endpoint}") private String gatewayEndpoint; + @Value("${fda.client_secret}") + private String clientSecret; + + @Value("${fda.client_id}") + private String clientId; + + @Value("${spring.rabbitmq.username}") private String brokerUsername; diff --git a/fda-database-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java b/fda-database-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java index 6094567aa88e3bdffed30248feeab60739a60837..dbdec859da2e21277ad45dc93732320db37e6a60 100644 --- a/fda-database-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java +++ b/fda-database-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java @@ -1,45 +1,60 @@ package at.tuwien.gateway.impl; -import at.tuwien.api.user.UserDto; +import at.tuwien.api.auth.TokenIntrospectDto; +import at.tuwien.api.user.UserDetailsDto; +import at.tuwien.config.GatewayConfig; import at.tuwien.gateway.AuthenticationServiceGateway; import at.tuwien.mapper.UserMapper; -import lombok.extern.log4j.Log4j2; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.*; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; import javax.servlet.ServletException; -@Log4j2 +@Slf4j @Service public class AuthenticationServiceGatewayImpl implements AuthenticationServiceGateway { private final UserMapper userMapper; private final RestTemplate restTemplate; + private final GatewayConfig gatewayConfig; @Autowired public AuthenticationServiceGatewayImpl(UserMapper userMapper, - @Qualifier("authenticationRestTemplate") RestTemplate restTemplate) { + @Qualifier("authenticationRestTemplate") RestTemplate restTemplate, + GatewayConfig gatewayConfig) { this.userMapper = userMapper; this.restTemplate = restTemplate; + this.gatewayConfig = gatewayConfig; } @Override public UserDetails validate(String token) throws ServletException { final HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + token); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + final MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); + body.add("client_secret", gatewayConfig.getClientSecret()); + body.add("client_id", gatewayConfig.getClientId()); + body.add("token", token); try { - final ResponseEntity<UserDto> response = restTemplate.exchange("/api/auth", HttpMethod.PUT, - new HttpEntity<>(null, headers), UserDto.class); - if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { + final ResponseEntity<TokenIntrospectDto> response = restTemplate.exchange("/api/auth/realms/dbrepo/protocol/openid-connect/token/introspect", HttpMethod.POST, + new HttpEntity<>(body, headers), TokenIntrospectDto.class); + if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to validate token with status code {}", response.getStatusCode()); - throw new ServletException("Failed to validate token"); + throw new ServletException("Failed to validate token: http status code is not ok"); + } else if (response.getBody() == null) { + throw new ServletException("Failed to validate token: body is null"); + } else if (!response.getBody().getActive()) { + throw new ServletException("Failed to validate token: token is not active"); } - final UserDetails dto = userMapper.userDtoToUserDetailsDto(response.getBody()); + final UserDetailsDto dto = userMapper.tokenIntrospectDtoToUserDetailsDto(response.getBody()); log.trace("gateway authenticated user {}", dto); return dto; } catch (HttpStatusCodeException e) { diff --git a/fda-database-service/services/src/main/java/at/tuwien/mapper/DatabaseMapper.java b/fda-database-service/services/src/main/java/at/tuwien/mapper/DatabaseMapper.java index 51e9c382be0e9fdf6616f0489ad69d590f16dd5f..3eb30d494652ef85af942dbd0a3957e08a9de37c 100644 --- a/fda-database-service/services/src/main/java/at/tuwien/mapper/DatabaseMapper.java +++ b/fda-database-service/services/src/main/java/at/tuwien/mapper/DatabaseMapper.java @@ -77,6 +77,7 @@ public interface DatabaseMapper { .get(0); return User.builder() .username(username) + .databasePassword(password) .build(); } diff --git a/fda-database-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-database-service/services/src/main/java/at/tuwien/mapper/UserMapper.java index e5f840d2019cf706ebd25654a0d7ad36bc5bba2b..c8c386119eb093501d958e654b5e23097839eafd 100644 --- a/fda-database-service/services/src/main/java/at/tuwien/mapper/UserMapper.java +++ b/fda-database-service/services/src/main/java/at/tuwien/mapper/UserMapper.java @@ -1,5 +1,6 @@ package at.tuwien.mapper; +import at.tuwien.api.auth.TokenIntrospectDto; import at.tuwien.api.user.GrantedAuthorityDto; import at.tuwien.api.user.UserBriefDto; import at.tuwien.api.user.UserDetailsDto; @@ -8,6 +9,9 @@ import org.mapstruct.Mapper; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import java.util.Arrays; +import java.util.stream.Collectors; + @Mapper(componentModel = "spring") public interface UserMapper { @@ -17,6 +21,16 @@ public interface UserMapper { UserBriefDto userDtoToUserBriefDto(UserDto data); + default UserDetailsDto tokenIntrospectDtoToUserDetailsDto(TokenIntrospectDto data) { + return UserDetailsDto.builder() + .id(data.getSub()) + .username(data.getUsername()) + .authorities(Arrays.stream(data.getRealmAccess().getRoles()) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList())) + .build(); + } + default GrantedAuthority grantedAuthorityDtoToGrantedAuthority(GrantedAuthorityDto data) { final GrantedAuthority authority = new SimpleGrantedAuthority(data.getAuthority()); log.trace("mapped granted authority {} to granted authority {}", data, authority); diff --git a/fda-ui/api/container/index.js b/fda-ui/api/container/index.js index 3cd8e7a2d83c6cbb086937b55b5f3343e7002acc..f5b6e59f80e395734784f08923d85adb48727ed4 100644 --- a/fda-ui/api/container/index.js +++ b/fda-ui/api/container/index.js @@ -4,6 +4,14 @@ export function listContainers (limit) { return axios.get(`/api/container?limit=${limit}`) } +export function createContainer (token, payload) { + return axios.post('/api/container/', payload, { + headers: { + Authorization: `Bearer ${token}` + } + }) +} + export function startContainer (token, containerId) { const payload = { action: 'start' diff --git a/fda-ui/api/database/index.js b/fda-ui/api/database/index.js index 7b61f97544d21309d765565765f4df380890c206..17d8bece4d303cfe1e7456f8eda96beaf94a6b33 100644 --- a/fda-ui/api/database/index.js +++ b/fda-ui/api/database/index.js @@ -1,11 +1,11 @@ const axios = require('axios/dist/browser/axios.cjs') -export function createDatabase (token, containerId) { +export function createDatabase (token, container) { const payload = { - name: null, - is_public: true + name: container.name, + is_public: container.is_public ? container.is_public : true } - return axios.post(`/api/container/${containerId}/database`, payload, { + return axios.post(`/api/container/${container.id}/database`, payload, { headers: { Authorization: `Bearer ${token}` } diff --git a/fda-ui/api/user/index.js b/fda-ui/api/user/index.js index 8894d2bfd1f14119c500bd88128975ee91e3238c..7e06253dec9b786d42f8a916b64242fc44913cb2 100644 --- a/fda-ui/api/user/index.js +++ b/fda-ui/api/user/index.js @@ -54,6 +54,11 @@ export function tokenToUser (token) { } } +export function tokenToExp (token) { + const data = jwt_decode(token) + return new Date(data.exp * 1000) +} + export function tokenToRoles (token) { const data = jwt_decode(token) return data.realm_access.roles diff --git a/fda-ui/components/.gitkeep b/fda-ui/components/.gitkeep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/fda-ui/components/DatabaseList.vue b/fda-ui/components/DatabaseList.vue index f3ffd4196bc6c17580a86a5d5a9a2cfcd4af503c..757d2be830618099451ea82033638535a60e30ce 100644 --- a/fda-ui/components/DatabaseList.vue +++ b/fda-ui/components/DatabaseList.vue @@ -14,7 +14,7 @@ </v-card-title> <v-card-subtitle v-if="!hasIdentifier(container)" class="db-subtitle" v-text="formatCreator(container.creator)" /> <v-card-subtitle v-if="hasIdentifier(container)" class="db-subtitle" v-text="formatCreatorz(container)" /> - <v-card-text class="db-description"> + <v-card-text v-if="hasDatabase(container)" class="db-description"> <div class="db-tags"> <v-chip v-if="hasDatabase(container) && container.database.is_public" small color="green" outlined>Public</v-chip> <v-chip v-if="hasDatabase(container) && !container.database.is_public" small color="red" outlined>Private</v-chip> @@ -36,7 +36,6 @@ Start </v-btn> </v-card-text> - <v-divider v-if="idx - 1 === databases.length" class="mx-4" /> </v-card> <v-toolbar v-if="false" flat> <v-toolbar-title> @@ -171,7 +170,7 @@ export default { async createDatabase (container) { try { container.loading = true - const res = await createDatabase(this.token, container.id) + const res = await createDatabase(this.token, container) container.database = res.data console.debug('created database', container.database) this.error = false diff --git a/fda-ui/components/dialogs/CreateDB.vue b/fda-ui/components/dialogs/CreateDB.vue index 320bcb0f78eef9c71e557b83b67fa9a0e0410b29..9e9a6ceaf8a846e79360bb2f112706b4069cef6e 100644 --- a/fda-ui/components/dialogs/CreateDB.vue +++ b/fda-ui/components/dialogs/CreateDB.vue @@ -55,6 +55,8 @@ <script> const { notEmpty } = require('@/utils') +const { createContainer, startContainer } = require('@/api/container') +const { createDatabase } = require('@/api/database') export default { data () { @@ -142,11 +144,12 @@ export default { this.createContainerDto.tag = this.engine.tag try { this.loading = true - const res = await this.$axios.post('/api/container', this.createContainerDto, this.config) + const res = await createContainer(this.token, this.createContainerDto) this.container = res.data console.debug('created container', this.container) this.error = false - } catch (err) { + } catch (error) { + console.error('create container', error) this.error = true this.$toast.error('Failed to create container') } @@ -155,7 +158,7 @@ export default { async startContainer (container) { try { this.loading = true - const res = await this.$axios.put(`/api/container/${container.id}`, { action: 'start' }, this.config) + const res = await startContainer(this.token, container.id) console.debug('started container', res.data) this.error = false } catch (error) { @@ -170,13 +173,15 @@ export default { async createDatabase (container) { try { this.loading = true + this.createDatabaseDto.id = container.id this.createDatabaseDto.name = container.name - const res = await this.$axios.post(`/api/container/${container.id}/database`, this.createDatabaseDto, this.config) + const res = await createDatabase(this.token, this.createDatabaseDto) container.database = res.data console.debug('created database', container.database) this.error = false this.$emit('close', { success: true }) - } catch (err) { + } catch (error) { + console.error('create database', error) this.error = true this.$toast.error('Failed to create database') } diff --git a/fda-ui/layouts/default.vue b/fda-ui/layouts/default.vue index 35ccd85ca99ad7e19d6f3ee7eb75127aa3b9360e..30700fe6ff746f656fba450a0feca346406d0c30 100644 --- a/fda-ui/layouts/default.vue +++ b/fda-ui/layouts/default.vue @@ -123,7 +123,7 @@ <script> import { isDeveloper } from '@/utils' -import { tokenToUser } from '@/api/user' +import { tokenToUser, tokenToExp, refresh } from '@/api/user' export default { name: 'DefaultLayout', data () { @@ -148,6 +148,9 @@ export default { token () { return this.$store.state.token }, + refreshToken () { + return this.$store.state.refreshToken + }, user () { return this.$store.state.user }, @@ -231,6 +234,7 @@ export default { if (this.$route.query && this.$route.query.q) { this.search = this.$route.query.q } + this.refreshTokenIfNecessary() }, methods: { submit () { @@ -240,8 +244,12 @@ export default { const redirect = ![undefined, '/', '/login'].includes(this.$router.currentRoute.path) this.$router.push({ path: '/login', query: redirect ? { redirect: this.$router.currentRoute.path } : {} }) }, - logout () { + logout (message = null) { + if (message) { + this.$toast.warning(message) + } this.$store.commit('SET_TOKEN', null) + this.$store.commit('SET_REFRESH_TOKEN', null) this.$store.commit('SET_USER', null) this.$store.commit('SET_ACCESS', null) this.$vuetify.theme.dark = false @@ -273,6 +281,36 @@ export default { } this.loadingUser = false }, + async refreshTokenIfNecessary () { + if (!this.token) { + return + } + const exp = tokenToExp(this.token) + if (exp > new Date()) { + console.debug('token will be refreshed', exp, 'timeout is', exp - new Date()) + setTimeout(() => this.refreshTokenIfNecessary(), exp - new Date()) + return + } + const refreshExp = tokenToExp(this.refreshToken) + if (refreshExp > new Date()) { + try { + const res = await refresh(this.clientSecret, this.refreshToken) + // eslint-disable-next-line camelcase + const { access_token, refresh_token } = res.data + this.$store.commit('SET_TOKEN', access_token) + this.$store.commit('SET_REFRESH_TOKEN', refresh_token) + console.info('refreshed tokens') + const user = tokenToUser(this.token) + console.debug('user', user) + this.$store.commit('SET_USER', user) + return + } catch (error) { + console.error('Failed to login', error) + this.$toast.error('Failed to refresh tokens') + } + } + this.logout('Your session has expired') + }, async loadDatabase () { if (!this.$route.params.container_id || !this.$route.params.database_id) { return diff --git a/fda-ui/pages/container/index.vue b/fda-ui/pages/container/index.vue index d83e211bc1a8a959317b907bf0b435a5cf0a91a6..a77355ab5f2936e6030593dd40a1ebe5b1c53e3f 100644 --- a/fda-ui/pages/container/index.vue +++ b/fda-ui/pages/container/index.vue @@ -73,22 +73,6 @@ export default { } }, methods: { - async createDatabase (container) { - try { - container.database.loading = true - this.createDatabaseDto.name = container.name - const res = await this.$axios.post(`/api/container/${container.id}/database`, this.createDatabaseDto, this.config) - container.database = res.data - console.debug('created database', container.database) - this.error = false - } catch (error) { - const { message } = error.response - this.error = true - console.error('Failed to create database', error) - this.$toast.error(`${message}`) - } - container.database.loading = false - }, closed (event) { this.createDbDialog = false if (event.success) { diff --git a/fda-ui/pages/login.vue b/fda-ui/pages/login.vue index 93af6d744a4d6f66dbc9c6fc813adf6aa4852d42..be8fbb1ed68323c63864093d25c0747945c31fe6 100644 --- a/fda-ui/pages/login.vue +++ b/fda-ui/pages/login.vue @@ -56,14 +56,14 @@ </template> <script> -import { authenticate, tokenToUser } from '@/api/user' +import { authenticate, refresh, tokenToExp, tokenToUser } from '@/api/user' export default { data () { return { loading: false, error: false, // XXX: `error` is never changed valid: false, - username: 'mweise', + username: null, password: null } }, @@ -74,6 +74,9 @@ export default { token () { return this.$store.state.token }, + refreshToken () { + return this.$store.state.refreshToken + }, user () { return this.$store.state.user }, @@ -107,11 +110,15 @@ export default { this.loading = true const res = await authenticate(this.clientSecret, this.username, this.password) console.debug('login user', res.data) - this.$store.commit('SET_TOKEN', res.data.access_token) + // eslint-disable-next-line camelcase + const { access_token, refresh_token } = res.data + this.$store.commit('SET_TOKEN', access_token) + this.$store.commit('SET_REFRESH_TOKEN', refresh_token) const user = tokenToUser(this.token) console.debug('user', user) this.$store.commit('SET_USER', user) this.$vuetify.theme.dark = user?.theme_dark || false + await this.refreshTokenIfNecessary() await this.$router.push({ path: this.$route.query.redirect ? this.$route.query.redirect : '/container' }) } catch (error) { console.error('Failed to login', error) @@ -129,6 +136,36 @@ export default { this.loading = false } }, + async refreshTokenIfNecessary () { + if (!this.token) { + return + } + const exp = tokenToExp(this.token) + if (exp > new Date()) { + console.debug('token will be refreshed', exp, 'timeout is', exp - new Date()) + setTimeout(() => this.refreshTokenIfNecessary(), exp - new Date()) + return + } + const refreshExp = tokenToExp(this.refreshToken) + if (refreshExp > new Date()) { + try { + const res = await refresh(this.clientSecret, this.refreshToken) + // eslint-disable-next-line camelcase + const { access_token, refresh_token } = res.data + this.$store.commit('SET_TOKEN', access_token) + this.$store.commit('SET_REFRESH_TOKEN', refresh_token) + console.info('refreshed tokens') + const user = tokenToUser(this.token) + console.debug('user', user) + this.$store.commit('SET_USER', user) + return + } catch (error) { + console.error('Failed to login', error) + this.$toast.error('Failed to refresh tokens') + } + } + this.logout('Your session has expired') + }, signup () { this.$router.push('/signup') }, diff --git a/fda-ui/pages/user/info.vue b/fda-ui/pages/user/info.vue index 7f0764650fcdf3d19b2b47a49da1af6481757c21..f1b8de891e7fcb82dbaa35b8fa07b1efbfb82629 100644 --- a/fda-ui/pages/user/info.vue +++ b/fda-ui/pages/user/info.vue @@ -68,12 +68,7 @@ <v-card-text> <v-row dense> <v-col> - <v-select - v-model="roles" - :items="roles" - multiple - chips - disabled /> + <pre>{{ roles.join(', ') }}</pre> </v-col> </v-row> </v-card-text> @@ -84,7 +79,7 @@ </template> <script> -import { UserToolbar } from '@/components/UserToolbar' +import UserToolbar from '@/components/UserToolbar' import { tokenToRoles } from '@/api/user' export default { diff --git a/fda-ui/plugins/vuex-persist.js b/fda-ui/plugins/vuex-persist.js index 9a3617376d35ee5687fe67392298cb477bc6a57f..e74b481794aad33bac5e27c38e4ecc26582fe864 100644 --- a/fda-ui/plugins/vuex-persist.js +++ b/fda-ui/plugins/vuex-persist.js @@ -5,6 +5,7 @@ export default ({ store }) => { storage: window.localStorage, reducer: state => ({ token: state.token, + refreshToken: state.refreshToken, user: state.user }) }).plugin(store) diff --git a/fda-ui/utils/index.js b/fda-ui/utils/index.js index 29c0e8385d09c11e92592062266d66069ef9b059..ce98464f42f4843294166b9398a27d872481def2 100644 --- a/fda-ui/utils/index.js +++ b/fda-ui/utils/index.js @@ -55,20 +55,12 @@ function formatUser (user) { return null } if (!('firstname' in user) || !('lastname' in user) || user.firstname === null || user.lastname === null) { - if (!('username' in user)) { + if (!('preferred_username' in user)) { return null } - return user.username + return user.preferred_username } - let name = '' - if (user.titles_before) { - name += user.titles_before + ' ' - } - name += user.firstname + ' ' + user.lastname - if (user.titles_after) { - name += ' ' + user.titles_after - } - return name + return user.firstname + ' ' + user.lastname } function formatDateUTC (str) {