diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-authentication-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index e7684780cda97e9bd74f6869b5fcd248b6d009c3..017992fda0e7314069d5fff2e461e88cfcd0c321 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -1,6 +1,5 @@ package at.tuwien.config; -import at.tuwien.auth.AuthEntrypoint; import at.tuwien.auth.AuthTokenFilter; import at.tuwien.auth.JwtUtils; import at.tuwien.service.impl.UserDetailsServiceImpl; diff --git a/fda-container-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-container-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..2d1d0258855ba5fa3444a8008e0db5a53594445c --- /dev/null +++ b/fda-container-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -0,0 +1,52 @@ +package at.tuwien.auth; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +@Component +public class AuthTokenFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String jwt = parseJwt(request); + if (jwt != null) { + + final UserDetails userDetails = userDetailsService.loadUserByUsername(username); + final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + /** + * Parses the token from the HTTP header of the request + * + * @param request The request. + * @return The token. + */ + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7, headerAuth.length()); + } + return null; + } +} \ No newline at end of file diff --git a/fda-container-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-container-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 9832234740ead10f80163b75958b5363a7789cd5..5f0bd4772059fb3410c182bffc929ac069f988aa 100644 --- a/fda-container-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/fda-container-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -1,6 +1,6 @@ package at.tuwien.config; -import org.springframework.beans.factory.annotation.Value; +import at.tuwien.auth.AuthTokenFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -9,7 +9,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @@ -21,8 +20,10 @@ import javax.servlet.http.HttpServletResponse; @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - @Value("${fda.auth.url}") - private String authUrl; + @Bean + public AuthTokenFilter authTokenFilter() { + return new AuthTokenFilter() + } @Override protected void configure(HttpSecurity http) throws Exception { @@ -45,11 +46,11 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ).and(); /* set permissions on endpoints */ http.authorizeRequests() + /* our public endpoints */ + .antMatchers(HttpMethod.GET, "/api/container/**").permitAll() + .antMatchers(HttpMethod.GET, "/api/image/**").permitAll() /* our private endpoints */ .anyRequest().authenticated(); - /* set auth url */ - http.formLogin() - .loginProcessingUrl(authUrl); } @Bean diff --git a/fda-ui/components/dialogs/CreateDB.vue b/fda-ui/components/dialogs/CreateDB.vue index 8f1dac773c8a193343670c2edca4b67b69c38782..1316c5fd642e920f302ed1b361c050897212b656 100644 --- a/fda-ui/components/dialogs/CreateDB.vue +++ b/fda-ui/components/dialogs/CreateDB.vue @@ -1,22 +1,22 @@ <template> <div> - <v-card> - <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" /> - <v-card-title> - Create Database - </v-card-title> - <v-card-text> - <v-alert - border="left" - color="amber lighten-4 black--text"> - Choose an expressive database name and select a database engine. - </v-alert> - <v-form v-model="formValid" autocomplete="off"> + <v-form ref="form" v-model="valid" @submit.prevent="submit"> + <v-card> + <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" /> + <v-card-title> + Create Database + </v-card-title> + <v-card-text> + <v-alert + border="left" + color="amber lighten-4 black--text"> + Choose an expressive database name and select a database engine. + </v-alert> <v-text-field id="database" v-model="database" name="database" - label="Database Name" + label="Name *" :rules="[v => !!v || $t('Required')]" required /> <v-textarea @@ -24,14 +24,14 @@ v-model="description" name="description" rows="2" - label="Database Description" + label="Description *" :rules="[v => !!v || $t('Required')]" required /> <v-select id="engine" v-model="engine" name="engine" - label="Database Engine" + label="Engine *" :items="engines" :item-text="item => `${item.repository}:${item.tag}`" :rules="[v => !!v || $t('Required')]" @@ -43,25 +43,26 @@ name="public" disabled label="Public" /> - </v-form> - </v-card-text> - <v-card-actions> - <v-spacer /> - <v-btn - class="mb-2" - @click="cancel"> - Cancel - </v-btn> - <v-btn - id="createDB" - class="mb-2" - :disabled="!formValid || loading" - color="primary" - @click="createDB"> - Create - </v-btn> - </v-card-actions> - </v-card> + </v-card-text> + <v-card-actions> + <v-spacer /> + <v-btn + class="mb-2" + @click="cancel"> + Cancel + </v-btn> + <v-btn + id="createDB" + class="mb-2" + :disabled="!valid || loading" + color="primary" + type="submit" + @click="createDB"> + Create + </v-btn> + </v-card-actions> + </v-card> + </v-form> </div> </template> @@ -69,7 +70,7 @@ export default { data () { return { - formValid: false, + valid: false, loading: false, error: false, database: null, @@ -83,12 +84,18 @@ export default { computed: { loadingColor () { return this.error ? 'red lighten-2' : 'primary' + }, + token () { + return this.$store.state.token } }, beforeMount () { this.getImages() }, methods: { + submit () { + this.$refs.form.validate() + }, cancel () { this.$parent.$parent.$parent.$parent.createDbDialog = false }, @@ -97,7 +104,9 @@ export default { try { this.loading = true this.error = false - res = await this.$axios.get('/api/image/') + res = await this.$axios.get('/api/image/', { + headers: { Authorization: `Bearer ${this.token}` } + }) this.engines = res.data console.debug('engines', this.engines) this.loading = false @@ -125,13 +134,22 @@ export default { description: this.description, repository: this.engine.repository, tag: this.engine.tag + }, { + headers: { Authorization: `Bearer ${this.token}` } }) containerId = res.data.id console.debug('created container', res.data) this.loading = false } catch (err) { this.error = true - this.$toast.error('Could not create container. Try another name.') + this.loading = false + if (err.status === 401) { + this.$toast.error('Authentication missing') + console.error('permission denied', err) + return + } + console.error('failed to create container', err) + this.$toast.error('Could not create container.') return } @@ -139,8 +157,8 @@ export default { try { this.loading = true this.error = false - res = await this.$axios.put(`/api/container/${containerId}`, { - action: 'START' + res = await this.$axios.put(`/api/container/${containerId}`, { action: 'START' }, { + headers: { Authorization: `Bearer ${this.token}` } }) console.debug('started container', res.data) } catch (err) { @@ -162,6 +180,8 @@ export default { name: this.database, description: this.description, is_public: this.isPublic + }, { + headers: { Authorization: `Bearer ${this.token}` } }) console.debug('created database', res) break diff --git a/fda-ui/layouts/default.vue b/fda-ui/layouts/default.vue index b92bb74530348900293fd5d50f3b62173d23d44e..158dbcb0985784d4a40295a1c3e89307054f17fb 100644 --- a/fda-ui/layouts/default.vue +++ b/fda-ui/layouts/default.vue @@ -29,11 +29,19 @@ <v-toolbar-title v-text="title" /> <v-spacer /> <v-btn + v-if="!token" class="mr-2 white--text" color="blue-grey" to="/login"> <v-icon left>mdi-login</v-icon> Login </v-btn> + <v-btn + v-if="!token" + class="mr-2 white--text" + color="primary" + to="/signup"> + <v-icon left>mdi-account-plus</v-icon> Signup + </v-btn> <v-menu bottom offset-y left> <template v-slot:activator="{ on, attrs }"> <v-btn @@ -44,22 +52,16 @@ </v-btn> </template> <v-list> - <v-list-item to="/signup"> - <v-list-item-icon> - <v-icon left>mdi-account-plus</v-icon> - </v-list-item-icon> - <v-list-item-title>Create Account</v-list-item-title> + <v-list-item + v-for="locale in availableLocales" + :key="locale.code" + :to="switchLocalePath(locale.code)"> + <v-list-item-title>{{ locale.name }}</v-list-item-title> + </v-list-item> + <v-list-item + @click="switchTheme()"> + {{ nextTheme }} Theme </v-list-item> - <!-- <v-list-item--> - <!-- v-for="locale in availableLocales"--> - <!-- :key="locale.code"--> - <!-- :to="switchLocalePath(locale.code)">--> - <!-- <v-list-item-title>{{ locale.name }}</v-list-item-title>--> - <!-- </v-list-item>--> - <!-- <v-list-item--> - <!-- @click="switchTheme()">--> - <!-- {{ nextTheme }} Theme--> - <!-- </v-list-item>--> </v-list> </v-menu> </v-app-bar> @@ -68,14 +70,14 @@ <nuxt /> </v-container> </v-main> - <v-footer v-if="sandbox" padless> + <v-footer padless> <v-card flat tile width="100%" - class="primary text-center"> - <v-card-text class="white--text"> - <strong>Sandbox Environment</strong> — <a href="//github.com/fair-data-austria/dbrepo/issues/new" class="white--text">Report a bug</a> + class="red lighten-1 text-center"> + <v-card-text class="black--text"> + This is a <strong>TEST</strong> environment, do not use production/confidential data! — <a href="//github.com/fair-data-austria/dbrepo/issues/new" class="black--text">Report a bug</a> </v-card-text> </v-card> </v-footer> @@ -145,8 +147,8 @@ export default { availableLocales () { return this.$i18n.locales.filter(i => i.code !== this.$i18n.locale) }, - sandbox () { - return true + token () { + return this.$store.state.token }, nextTheme () { return this.$vuetify.theme.dark ? 'Light' : 'Dark' diff --git a/fda-ui/pages/container/index.vue b/fda-ui/pages/container/index.vue index fe106920f7622f9946aa3eef48a61c2ab23680fb..ed25283940084a74547f640858b6490992b69289 100644 --- a/fda-ui/pages/container/index.vue +++ b/fda-ui/pages/container/index.vue @@ -7,7 +7,7 @@ </v-toolbar-title> <v-spacer /> <v-toolbar-title> - <v-btn color="primary" @click.stop="createDbDialog = true"> + <v-btn v-if="token" color="primary" @click.stop="createDbDialog = true"> <v-icon left>mdi-plus</v-icon> Database </v-btn> </v-toolbar-title> @@ -89,9 +89,7 @@ export default { this.createDbDialog = false try { this.loading = true - let res = await this.$axios.get('/api/container/', { - headers: { Authorization: `Bearer ${this.token}` } - }) + let res = await this.$axios.get('/api/container/') this.containers = res.data console.debug('containers', this.containers) for (const container of this.containers) { diff --git a/fda-ui/pages/login.vue b/fda-ui/pages/login.vue index 2de5fb7c1404e7f8af7835e7ef40c7d58cd11014..f6a07b3f4d4d8bfb32f25efe4abef45501b128b2 100644 --- a/fda-ui/pages/login.vue +++ b/fda-ui/pages/login.vue @@ -1,45 +1,53 @@ <template> <div> - <v-card> - <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" /> - <v-card-title> - Login - </v-card-title> - <v-card-text> - <v-form ref="form" v-model="valid"> + <v-form ref="form" v-model="valid" @submit.prevent="submit"> + <v-card v-if="!token"> + <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" /> + <v-card-title> + Login + </v-card-title> + <v-card-text> + <v-alert + border="left" + color="amber lighten-4 black--text"> + If you need an account, create one <a @click="signup">here</a>. + </v-alert> <v-row> - <v-col> + <v-col cols="6"> <v-text-field v-model="loginAccount.username" autocomplete="off" + required :rules="[v => !!v || $t('Required')]" label="Username *" /> </v-col> </v-row> <v-row> - <v-col> + <v-col cols="6"> <v-text-field v-model="loginAccount.password" autocomplete="off" type="password" + required :rules="[v => !!v || $t('Required')]" label="Password *" /> </v-col> </v-row> - </v-form> - </v-card-text> - <v-card-actions> - <v-spacer /> - <v-btn - id="login" - class="mb-2" - :disabled="!valid" - color="primary" - @click="login"> - Login - </v-btn> - </v-card-actions> - </v-card> + </v-card-text> + <v-card-actions> + <v-btn + id="login" + class="mb-2 ml-2" + :disabled="!valid" + color="primary" + type="submit" + @click="login"> + Login + </v-btn> + </v-card-actions> + </v-card> + </v-form> + <p v-if="token">Already logged-in</p> </div> </template> @@ -59,11 +67,17 @@ export default { computed: { loadingColor () { return this.error ? 'red lighten-2' : 'primary' + }, + token () { + return this.$store.state.token } }, beforeMount () { }, methods: { + submit () { + this.$refs.form.validate() + }, async login () { const url = '/api/auth' try { @@ -72,11 +86,15 @@ export default { console.debug('login user', res.data) this.$store.commit('SET_TOKEN', res.data.token) this.$toast.success('Welcome back!') + this.$router.push('/container') } catch (err) { console.error('login user failed', err) this.$toast.error('Failed to login user') } this.loading = false + }, + signup () { + this.$router.push('/signup') } } } diff --git a/fda-ui/pages/signup.vue b/fda-ui/pages/signup.vue index 8eecba91341c5f82bf1317ccedb699a00de6aa12..15b318ac8047f07e730ef285f159f250d55487d9 100644 --- a/fda-ui/pages/signup.vue +++ b/fda-ui/pages/signup.vue @@ -1,80 +1,87 @@ <template> <div> - <v-card> - <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" /> - <v-card-title> - Create Account - </v-card-title> - <v-card-text> - <v-alert - border="left" - color="amber lighten-4 black--text"> - Before you can use the repository sandbox, you will need to <i>confirm</i> your email address, make sure to check your spam folder. - </v-alert> - <v-alert - border="left" - color="red lighten-1 black--text"> - This is a <strong>TEST</strong> environment, do not use production/confidential data! - </v-alert> - <v-form ref="form" v-model="valid"> + <v-form ref="form" v-model="valid" @submit.prevent="submit"> + <v-card> + <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" /> + <v-card-title> + Create Account + </v-card-title> + <v-card-text> + <v-alert + border="left" + color="amber lighten-4 black--text"> + Before you can use the repository sandbox, you will need to <i>confirm</i> your email address, make sure to check your spam folder. + </v-alert> <v-row> - <v-col> + <v-col cols="6"> <v-text-field v-model="createAccount.email" type="email" autocomplete="off" + required :rules="[v => !!v || $t('Required')]" hint="e.g. max.mustermann@work.com" label="Work E-Mail Address *" /> </v-col> </v-row> <v-row> - <v-col> + <v-col cols="6"> <v-text-field v-model="createAccount.username" autocomplete="off" + required :rules="[v => !!v || $t('Required')]" hint="e.g. mmustermann" label="Username *" /> </v-col> </v-row> <v-row> - <v-col> + <v-col cols="6"> <v-text-field v-model="createAccount.password" autocomplete="off" + required :rules="[v => !!v || $t('Required')]" type="password" label="Password *" /> </v-col> </v-row> <v-row> - <v-col> + <v-col cols="6"> <v-checkbox v-model="consent" + required :rules="[v => !!v || $t('Required')]" label="I understand the warning and do not use production data" /> </v-col> </v-row> - </v-form> - </v-card-text> - <v-card-actions> - <v-spacer /> - <v-btn - class="mb-2" - @click="cancel"> - Cancel - </v-btn> - <v-btn - id="login" - class="mb-2" - :disabled="!valid" - color="primary" - @click="register"> - Submit - </v-btn> - </v-card-actions> - </v-card> + <v-row> + <v-col cols="6"> + <v-checkbox + v-model="privacy" + required + :rules="[v => !!v || $t('Required')]" + label="I have read and accept the privacy statement" /> + </v-col> + </v-row> + </v-card-text> + <v-card-actions> + <v-btn + class="mb-2 ml-2"> + Cancel + </v-btn> + <v-btn + id="login" + class="mb-2" + :disabled="!valid" + color="primary" + type="submit" + @click="register"> + Submit + </v-btn> + </v-card-actions> + </v-card> + </v-form> </div> </template> @@ -85,6 +92,7 @@ export default { loading: false, error: false, valid: false, + privacy: false, consent: false, createAccount: { username: null, @@ -101,6 +109,9 @@ export default { beforeMount () { }, methods: { + submit () { + this.$refs.form.validate() + }, async register () { const url = '/api/user' try { @@ -108,6 +119,7 @@ export default { const res = await this.$axios.post(url, this.createAccount) console.debug('create user', res.data) this.$toast.success('Success. Check your inbox!') + this.$router.push('/login') } catch (err) { console.error('create user failed', err) this.$toast.error('Failed to create user')