Gerüst mit Form-Auth

This commit is contained in:
mbremer
2021-07-02 21:39:28 +02:00
commit 825363e491
30 changed files with 1528 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./mvnw package
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/buerokalender-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/buerokalender-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/buerokalender-jvm
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
&& microdnf update \
&& microdnf clean all \
&& mkdir /deployments \
&& chown 1001 /deployments \
&& chmod "g+rwX" /deployments \
&& chown 1001:root /deployments \
&& curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
&& chown 1001 /deployments/run-java.sh \
&& chmod 540 /deployments/run-java.sh \
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=1001 target/quarkus-app/*.jar /deployments/
COPY --chown=1001 target/quarkus-app/app/ /deployments/app/
COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 1001
ENTRYPOINT [ "/deployments/run-java.sh" ]

View File

@@ -0,0 +1,51 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./mvnw package -Dquarkus.package.type=legacy-jar
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/buerokalender-legacy-jar .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/buerokalender-legacy-jar
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/buerokalender-legacy-jar
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
&& microdnf update \
&& microdnf clean all \
&& mkdir /deployments \
&& chown 1001 /deployments \
&& chmod "g+rwX" /deployments \
&& chown 1001:root /deployments \
&& curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
&& chown 1001 /deployments/run-java.sh \
&& chmod 540 /deployments/run-java.sh \
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
COPY target/lib/* /deployments/lib/
COPY target/*-runner.jar /deployments/app.jar
EXPOSE 8080
USER 1001
ENTRYPOINT [ "/deployments/run-java.sh" ]

View File

@@ -0,0 +1,27 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode
#
# Before building the container image run:
#
# ./mvnw package -Pnative
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t quarkus/buerokalender .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/buerokalender
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root target/*-runner /work/application
EXPOSE 8080
USER 1001
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

View File

@@ -0,0 +1,23 @@
####
# This Dockerfile is used in order to build a distroless container that runs the Quarkus application in native (no JVM) mode
#
# Before building the container image run:
#
# ./mvnw package -Pnative
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native-distroless -t quarkus/buerokalender .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/buerokalender
#
###
FROM quay.io/quarkus/quarkus-distroless-image:1.0
COPY target/*-runner /application
EXPOSE 8080
USER nonroot
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

View File

@@ -0,0 +1,26 @@
package de.mbremer.kalender;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/kalender")
@RolesAllowed({"USER", "ADMIN"})
public class KalenderResource {
@Inject
Template kalender;
@Path("")
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance kalender() {
return kalender.instance();
}
}

View File

@@ -0,0 +1,20 @@
package de.mbremer.kalender;
import de.mbremer.secutity.User;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import java.time.LocalDate;
@Getter
@Setter
public class KalenderTag extends PanacheEntity {
private LocalDate date;
@OneToMany
private User inOffice;
}

View File

@@ -0,0 +1,5 @@
package de.mbremer.secutity;
public enum Role {
ADMIN, USER
}

View File

@@ -0,0 +1,57 @@
package de.mbremer.secutity;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
@UserDefinition
@NoArgsConstructor
@Entity
@Table(name = "users")
public class User extends PanacheEntity {
@Username
@NotBlank
@Getter
@Column(nullable = false)
private String username;
@Password
@NotBlank
@Getter
@Column(nullable = false)
private String password;
/**
* ADMIN or USER.
*/
@Roles
@Getter
@Setter
@Column(nullable = false)
private String role = "USER";
public User setPassword(String password) {
this.password = BcryptUtil.bcryptHash(password);
return this;
}
public User setUsername(String username) {
this.username = username == null ? null : username.trim();
return this;
}
public User setRole(String role) {
this.role = role == null ? role : role.toUpperCase();
return this;
}
}

View File

@@ -0,0 +1,18 @@
package de.mbremer.secutity;
import javax.ws.rs.FormParam;
public class UserForm {
public @FormParam("username") String username;
public @FormParam("password") String password;
public @FormParam("passwordVerify") String passwordVerify;
public @FormParam("role") String role;
public User getUser() {
return new User().setUsername(username).setPassword(password).setRole(role);
}
public boolean verifyPassword() {
return password != null && password.length() > 4 && password.equals(passwordVerify);
}
}

View File

@@ -0,0 +1,104 @@
package de.mbremer.secutity;
import io.quarkus.qute.Location;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.security.identity.SecurityIdentity;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.providers.multipart.MultipartForm;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import java.net.URI;
import static de.mbremer.secutity.Role.ADMIN;
@Path("/user")
@RolesAllowed("ADMIN")
public class UserResource {
@Inject
Logger log;
@Inject
SecurityIdentity identity;
@Inject
Template userinit;
@Inject
@Location("user.html")
Template userTemplate;
@GET
@Produces(MediaType.TEXT_HTML)
@RolesAllowed({"USER", "ADMIN"})
public TemplateInstance getUser() {
return userTemplate
.data("user_count", User.count())
.data("current_username", identity.getPrincipal().getName())
.data("is_admin", identity.hasRole("ADMIN"));
}
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Transactional
@Path("init")
@PermitAll
public Response initialAdd(@MultipartForm UserForm userForm) {
if(User.count("role", ADMIN.name()) > 0) {
throw new BadRequestException("Only allowed on initial set up");
}
if(!ADMIN.name().equals(userForm.role)) {
throw new BadRequestException("Only role ADMIN");
}
User user = userForm.getUser();
user.persist();
return Response.status(Response.Status.SEE_OTHER)
.location(URI.create("/login.html"))
.build();
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("init")
@PermitAll
public TemplateInstance initial() {
if(User.count("role", ADMIN.name()) > 0) {
throw new BadRequestException("Only allowed on initial set up");
}
return userinit.instance();
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("logout")
@RolesAllowed({"USER", "ADMIN"})
public Response logout() {
return Response
.temporaryRedirect(URI.create("/"))
.cookie(new NewCookie("quarkus-credential", null, "/", null, null, 0, false,true))
.build();
}
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_HTML)
@Transactional
@Path("/new")
public TemplateInstance add(@MultipartForm UserForm userForm) {
if (!userForm.verifyPassword()) {
return getUser().data("error", "Das Passwort ist zu kurz oder stimmt nicht mit der Wiederholung überein.");
}
User user = userForm.getUser();
user.persist();
return getUser().data("info", "User angelegt.");
}
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/webjars/bootstrap/5.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="/webjars/bootstrap-icons/1.5.0/font/bootstrap-icons.css">
<title>Bürokalender</title>
</head>
<body>
<div class="container-lg fs-5">
<div class="alert alert-danger" role="alert">
Ups, da klappt etwas nicht!
</div>
<div>
<a class="btn btn-primary" href="/" role="button">Home</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta http-equiv="Refresh" content="0; url=/kalender"/>
<title>Bürokalender</title>
</head>
<body>
<p>Weiterleitung auf die <a href="/kalender">Startseite</a></p></body>
</html>

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/webjars/bootstrap/5.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="/webjars/bootstrap-icons/1.5.0/font/bootstrap-icons.css">
<title>Bürokalender</title>
</head>
<body>
<div class="container col-xl-10 p-3">
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3">
<div class="navbar-brand"><i class="bi-puzzle"></i> Bürokalender</div>
</nav>
<div class="row align-items-center g-lg-5 py-5">
<div class="col-md-10 mx-auto col-lg-5">
<form class="p-4 p-md-5 border rounded-3 bg-light" action="j_security_check" method="post">
<div class="form-floating mb-3">
<input type="text" name="j_username" class="form-control" id="floatingInput" placeholder="name@example.com">
<label for="floatingInput">Benutzername</label>
</div>
<div class="form-floating mb-3">
<input type="password" name="j_password" class="form-control" id="floatingPassword" placeholder="Password">
<label for="floatingPassword">Passwort</label>
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Login</button>
</form>
</div>
</div>
</div>
<script src="/webjars/bootstrap/5.0.0/js/bootstrap.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
# Database
%prod.quarkus.datasource.jdbc.url = jdbc:postgresql://db:5432/buerokalender
%prod.quarkus.datasource.username = buerokalender
%prod.quarkus.datasource.password = buerokalender
%prod.quarkus.datasource.jdbc.max-size=16
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQL95Dialect
quarkus.flyway.migrate-at-start=true
# Security
quarkus.http.auth.form.enabled=true
quarkus.http.auth.session.encryption-key=zHId14V+uiyxmbzhEPCyi7VvbaI80UeEO5yu0H/hVLs=
# 24h
quarkus.http.auth.form.timeout=86400

View File

@@ -0,0 +1,14 @@
create sequence hibernate_sequence start 1;
create table users (
id bigint not null primary key,
username varchar not null unique,
password varchar not null,
role varchar not null
);
create table kalendertag (
id bigint not null primary key,
date date not null unique,
inoffice_id bigint not null references users
);

View File

@@ -0,0 +1,33 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/webjars/bootstrap/5.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="/webjars/bootstrap-icons/1.5.0/font/bootstrap-icons.css">
<title>Bürokalender</title>
</head>
<body>
<div class="container col-xl-10 p-3">
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-brand ml-2"><i class="bi-puzzle"></i> Bürokalender</div>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<div class="navbar-nav mr-auto">
<a class="nav-item nav-link {#insert kalender_active}{/}" href="/kalender">Kalender</a>
<a class="nav-item nav-link {#insert user_active}{/}" href="/user">Benutzer</a>
</div>
</div>
<a class="btn btn-outline-primary me-2" href="/user/logout">Logout</a>
</nav>
{#if error}<div class="alert alert-danger" role="alert">{error}</div>{/if}
{#if info}<div class="alert alert-primary" role="alert">{info}</div>{/if}
{#insert contents}No contents!{/}
</div>
<script src="/webjars/bootstrap/5.0.0/js/bootstrap.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
{#include base.html}
{#kalender_active}active{/}
{#contents}
<div class="mt-2">
aktuelle Woche<br>
nächste Woche<br>
übernächste Woche<br>
</div>
{/contents}
{/include}

View File

@@ -0,0 +1,29 @@
<div class="modal fade" id="passwordModal" tabindex="-1" aria-labelledby="passwordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userModalLabel">Passwort ändern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="/user/password" method="POST" name="userForm" enctype="multipart/form-data">
<div class="modal-body row mb-3">
<div class="align-items-center col-md-10 mx-auto col-lg-11">
<div class="form-floating mb-3">
<input type="password" name="password" class="form-control" id="pwd" placeholder="Password" required>
<label for="pwd">Passwort</label>
</div>
<div class="form-floating mb-3">
<input type="password" name="passwordVerify" class="form-control" id="pwdv" placeholder="Passwort wiederholen" required>
<label for="pwd">Passwort wiederholen</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,40 @@
<div class="modal fade" id="userModal" tabindex="-1" aria-labelledby="userModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userModalLabel">Benutzer anlegen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="/user/new" method="POST" name="userForm" enctype="multipart/form-data">
<div class="modal-body row mb-3">
<div class="align-items-center col-md-10 mx-auto col-lg-11">
<div class="form-floating mb-3">
<input type="text" name="username" class="form-control" id="name" placeholder="Benutzername" required>
<label for="name">Benutzername</label>
</div>
<div class="form-floating mb-3">
<input type="password" name="password" class="form-control" id="pwd" placeholder="Password" required>
<label for="pwd">Passwort</label>
</div>
<div class="form-floating mb-3">
<input type="password" name="passwordVerify" class="form-control" id="pwdv" placeholder="Passwort wiederholen" required>
<label for="pwd">Passwort wiederholen</label>
</div>
<div class="form-floating mb-3">
<select id="role" name="role" class="form-select" required>
<option selected>USER</option>
<option>ADMIN</option>
</select>
<label class="col-sm-3 col-form-label1" for="role">Rolle</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,21 @@
{#include base.html}
{#user_active}active{/}
{#contents}
<div class="mt-2">
<h2>Hallo {current_username}</h2>
{#if is_admin}User: {user_count}{/if}
</div>
<div class="mt-2" >
{#if is_admin}
<a class="btn btn-primary btn" data-bs-toggle="modal" data-bs-target="#userModal" role="button">neuer Benutzer</a>
{/if}
<a class="btn btn-primary btn" data-bs-toggle="modal" data-bs-target="#passwordModal" role="button">Passwort ändern</a>
{#if is_admin}
{#include user-modal.html}{/include}
{/if}
{#include password-modal.html}{/include}
</div>
{/contents}
{/include}

View File

@@ -0,0 +1,30 @@
{#include base.html}
{#contents}
<div class="container mt-2">
<p>Die Anwendung kennt noch keinen Benutzer.</p>
<p><b>Bitte lege einen Admin-Benutzer an.</b></p>
<form action="/user/init" method="POST" name="userForm" enctype="multipart/form-data">
<div class="form-row align-items-center">
<div class="col-sm-3 my-1">
<label class="sr-only" for="name">Name</label>
<input type="text" name="username" class="form-control" id="name" placeholder="Benutzername" required autofocus>
</div>
<div class="col-sm-3 my-1">
<label class="sr-only" for="pwd">Passwort</label>
<input type="password" name="password" class="form-control" id="pwd" placeholder="Passwort" required>
</div>
<div class="col-sm-3 my-1">
<label class="sr-only" for="pwd">Rolle</label>
<input type="text" name="role" class="form-control" id="role" value="ADMIN" readonly>
</div>
</div>
<div>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
{/contents}
{/include}