Skip to content

Commit

Permalink
#189 add simple authentication and cli command to add new user
Browse files Browse the repository at this point in the history
  • Loading branch information
Thorsten Marx committed Apr 25, 2024
1 parent 099d0a9 commit 13e55d0
Show file tree
Hide file tree
Showing 21 changed files with 713 additions and 143 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

Expand All @@ -34,6 +32,9 @@
* @author thmar
*/
public interface DBFileSystem {

Path base();

Path resolve(String path);

String loadContent(final Path file) throws IOException;
Expand Down
28 changes: 28 additions & 0 deletions cms-auth/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.thmarx.cms</groupId>
<artifactId>cms-parent</artifactId>
<version>4.16.0</version>
</parent>
<artifactId>cms-auth</artifactId>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>com.github.thmarx.cms</groupId>
<artifactId>cms-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.thmarx.cms</groupId>
<artifactId>cms-filesystem</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.github.thmarx.cms.auth.services;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.yaml.snakeyaml.Yaml;

/*-
* #%L
* cms-auth
* %%
* Copyright (C) 2023 - 2024 Marx-Software
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/

/**
*
* @author t.marx
*/
@Slf4j
@RequiredArgsConstructor
public class AuthService {

private static final String FILENAME = "auth.yaml";

private final Path hostsBase;

public Optional<Auth> load () {
var authFile = hostsBase.resolve("config/auth.yaml");
if (!Files.exists(authFile)) {
return Optional.empty();
}
try (InputStream in = Files.newInputStream(authFile)) {
return Optional.ofNullable(new Yaml().loadAs(in, Auth.class));
} catch (Exception e) {
log.error("error loading auth file", e);
return Optional.empty();
}
}


@AllArgsConstructor
@NoArgsConstructor
@Data
public static class Auth {

private List<AuthPath> paths;

public Optional<AuthPath> find (final String path) {
return paths.stream().filter(secPath -> path.startsWith(secPath.path)).findFirst();
}
}

@AllArgsConstructor
@NoArgsConstructor
@Data
public static class AuthPath {
private String path;
private String realm;
private List<String> groups;

public boolean allowed (UserService.User user) {
if (user.groups() == null || user.groups().length == 0) {
return false;
}
if (groups == null || groups.isEmpty()) {
return false;
}

for (String group : user.groups()) {
if (groups.contains(group)) {
return true;
}
}

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.github.thmarx.cms.auth.services;

/*-
* #%L
* cms-api
* %%
* Copyright (C) 2023 - 2024 Marx-Software
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/

import com.github.thmarx.cms.auth.utils.SecurityUtil;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
*
* @author t.marx
*/
@Slf4j
@RequiredArgsConstructor
public class UserService {
private static final String FILENAME_PATTERN = "%s.realm";

private final static Splitter userSplitter = Splitter.on(":").trimResults();
private final static Splitter groupSplitter = Splitter.on(",").trimResults();

private final Path hostBase;

public void addUser (String realm, String username, String password, String [] groups) throws IOException {
saveUser(new User(username, SecurityUtil.hash(password), groups), realm);
}

private static User fromString(final String userString) {
List<String> userParts = userSplitter.splitToList(userString);

var username = userParts.get(0);
var passwordHash = userParts.get(1);
var groups = Iterables.toArray(groupSplitter.split(userParts.get(2)), String.class);

return new User(username, passwordHash, groups);
}

private List<User> loadUsers(final String realm) throws IOException {
Path usersFile = hostBase.resolve("config/" + FILENAME_PATTERN.formatted(realm));
List<User> users = new ArrayList<>();
if (Files.exists(usersFile)) {
List<String> lines = Files.readAllLines(usersFile, StandardCharsets.UTF_8);

for (String line : lines) {
if (!line.startsWith("#")) {
try {
users.add(fromString(line));
} catch (Exception e) {
log.error("error loading user", e);
}
}
}
}

return users;
}

public Optional<User> login(final String username, final String password, final String realm) {
try {
final String hashedPassword = SecurityUtil.hash(password);

var userOpt = loadUsers(realm).stream().filter(user -> user.username().equals(username)).findFirst();
if (
userOpt.isPresent()
&& userOpt.get().passwordHash.equals(hashedPassword)) {
return userOpt;
}

return Optional.empty();
} catch (Exception ex) {
log.error("", ex);
}
return Optional.empty();
}

private void saveUser(User user, final String realm) throws IOException {
Path usersFile = hostBase.resolve("config/" + FILENAME_PATTERN.formatted(realm));
if (Files.exists(usersFile)) {
Files.createFile(usersFile);
}

Files.writeString(usersFile, user.line(), StandardCharsets.UTF_8, StandardOpenOption.CREATE);
}

public static record User (String username, String passwordHash, String[] groups) {

public String line () {
return "%s:%s:%s\r\n".formatted(
username,
passwordHash,
groups!= null ? String.join(",", groups) : ""
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.github.thmarx.cms.auth.utils;

/*-
* #%L
* cms-api
* %%
* Copyright (C) 2023 - 2024 Marx-Software
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/

import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import java.security.SecureRandom;

/**
*
* @author t.marx
*/
public class SecurityUtil {

private static final SecureRandom RANDOM = new SecureRandom();

public static String hash (final String value) {
return Hashing.sha256()
.hashString(value, Charsets.UTF_8)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.github.thmarx.cms.auth.services;

/*-
* #%L
* cms-auth
* %%
* Copyright (C) 2023 - 2024 Marx-Software
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/

import java.nio.file.Path;
import java.util.Optional;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;


/**
*
* @author t.marx
*/
public class AuthServiceTest {

@Test
public void no_auth() {

var authService = new AuthService(Path.of("src/test/resources/hosts/none"));

var auth = authService.load();
Assertions.assertThat(auth).isEmpty();
}

@Test
public void load_auth() {

var authService = new AuthService(Path.of("src/test/resources/hosts/demo"));

var auth = authService.load();
Assertions.assertThat(auth).isPresent();

Optional<AuthService.AuthPath> find = auth.get().find("/secured");

Assertions.assertThat(find).isPresent();
}
}
Loading

0 comments on commit 13e55d0

Please sign in to comment.