Commit fe4b0a46 authored by Nicolas Pernoud's avatar Nicolas Pernoud
Browse files

fix: do not display password hash in ui

chore: refactored all js components as classes
fix: correct redirection with https schemes
parent f658cc60
Pipeline #13960 passed with stages
in 2 minutes and 52 seconds
......@@ -706,9 +706,9 @@
"dev": true
},
"node_modules/hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"node_modules/http-signature": {
......@@ -2633,9 +2633,9 @@
"dev": true
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"http-signature": {
......
......@@ -13,6 +13,7 @@ import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strconv"
"strings"
......@@ -144,25 +145,26 @@ func parseApps(file string, authz authzFunc) ([]*app, error) {
// makeHandler constructs the appropriate Handler for the given app.
func makeHandler(app *app, authz authzFunc) http.Handler {
var handler http.Handler
if fwdTo := app.ForwardTo; app.IsProxy && fwdTo != "" {
fwdToWithScheme := app.ForwardTo
if !strings.Contains(app.ForwardTo, "://") {
fwdToWithScheme = "http://" + app.ForwardTo // Scheme default to http
}
fwdToURL, err := url.Parse(fwdToWithScheme)
if err != nil {
log.Logger.Printf("app %v forward to a malformed url (%v), skipping", app.Name, app.ForwardTo)
return handler
}
if app.IsProxy {
fwdFrom := strings.TrimPrefix(app.Host, "*.")
handler = &httputil.ReverseProxy{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
Director: func(req *http.Request) {
// Set the correct scheme to the request
if !strings.HasPrefix(fwdTo, "http") {
req.URL.Scheme = "http"
req.URL.Host = fwdTo
} else {
fwdToSplit := strings.Split(fwdTo, "://")
req.URL.Scheme = fwdToSplit[0]
req.URL.Host = fwdToSplit[1]
}
// Rewrite host header if the proxy is not to a local service
if !strings.Contains(fwdTo, ":") {
req.Host = fwdTo
req.URL.Scheme = fwdToURL.Scheme
req.URL.Host = fwdToURL.Host
if fwdToURL.Port() == "" { // If the target service contains no port, is to an external service and we need to rewrite the host header to fool the target site
req.Host = fwdToURL.Host
}
if app.Login != "" && app.Password != "" {
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(app.Login+":"+app.Password)))
......@@ -171,8 +173,7 @@ func makeHandler(app *app, authz authzFunc) http.Handler {
ModifyResponse: func(res *http.Response) error {
u, err := res.Location()
if err == nil {
// Alter the redirect location if the redirection is relative to the proxied host
if strings.Contains(u.Host, fwdTo) {
if strings.Contains(u.Host, fwdToURL.Hostname()) { // Alter the redirect location if the redirection is relative to the proxied host
u.Scheme = "https"
u.Host = fwdFrom + ":" + strconv.Itoa(port)
}
......
......@@ -47,8 +47,14 @@ func TestAddUser(t *testing.T) {
defer os.Remove(UsersFile)
handler := http.HandlerFunc(AddUser)
tester.DoRequestOnHandler(t, handler, "POST", "/", noH, `{"id":"1","login":"admin","password": "password"}`, http.StatusOK, `[{"id":"1","login":"admin"`)
tester.DoRequestOnHandler(t, handler, "POST", "/", noH, `{"id":"1","login":"admin","password": ""}`, http.StatusBadRequest, `passwords cannot be blank`)
// Alter the password of the admin user, must create an hash
tester.DoRequestOnHandler(t, handler, "POST", "/", noH, `{"id":"1","login":"admin","password": "password"}`, http.StatusOK, `[{"id":"1","login":"admin","memberOf":null,"passwordHash":"$2a`)
// Test that altering an user without altering the password keep the password hash
tester.DoRequestOnHandler(t, handler, "POST", "/", noH, `{"id":"1","login":"admin_altered"}`, http.StatusOK, `[{"id":"1","login":"admin_altered","memberOf":null,"passwordHash":"$2a`)
// Add a new user with a password, must pass
tester.DoRequestOnHandler(t, handler, "POST", "/", noH, `{"id":"3","login":"user3","password": "password_user3"}`, http.StatusOK, `[{"id":"1","login":"admin`)
// Add a new user with no password, must fail
tester.DoRequestOnHandler(t, handler, "POST", "/", noH, `{"id":"4","login":"user4","password": ""}`, http.StatusBadRequest, `password cannot be empty`)
}
func TestMatchUser(t *testing.T) {
......@@ -96,8 +102,8 @@ func TestMatchUser(t *testing.T) {
func writeUsers() (name string) {
users := []*User{
{ID: "1", Login: "admin", Password: "password"},
{ID: "2", Login: "user", Password: "password"},
{ID: "1", Login: "admin"},
{ID: "2", Login: "user"},
}
f, err := ioutil.TempFile("", "users")
if err != nil {
......
......@@ -129,10 +129,7 @@ func AddUser(w http.ResponseWriter, req *http.Request) {
return
}
// Encrypt the password with bcrypt
if newUser.Password == "" && newUser.PasswordHash == "" {
http.Error(w, "passwords cannot be blank", http.StatusBadRequest)
return
}
samePassword := true
if newUser.Password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(newUser.Password), bcrypt.DefaultCost)
if err != nil {
......@@ -141,11 +138,15 @@ func AddUser(w http.ResponseWriter, req *http.Request) {
}
newUser.PasswordHash = string(hash)
newUser.Password = ""
samePassword = false
}
// Add the user only if the id doesn't exists yet
isNew := true
for idx, val := range users {
if val.ID == newUser.ID {
if samePassword { // If user exists, and no new password was provided, keep the existing password
newUser.PasswordHash = users[idx].PasswordHash
}
users[idx] = newUser
isNew = false
} else if val.Login == newUser.Login { // Check for already existing login
......@@ -153,6 +154,10 @@ func AddUser(w http.ResponseWriter, req *http.Request) {
return
}
}
if newUser.PasswordHash == "" {
http.Error(w, "password cannot be empty", http.StatusBadRequest)
return
}
if isNew {
users = append(users, newUser)
sort.Sort(ByID(users))
......
This diff is collapsed.
This diff is collapsed.
......@@ -2,10 +2,10 @@
import { AnimateCSS, RandomString, GID } from "/services/common/common.js";
import { HandleError } from "/services/common/errors.js";
import { Share } from "/components/davs/share.js";
import * as Auth from "/services/auth/auth.js";
export class Edit {
constructor(hostname, file) {
constructor(user, hostname, file) {
this.user = user;
this.hostname = hostname;
this.file = file;
this.url = `${hostname}${file.path}`;
......@@ -18,7 +18,6 @@ export class Edit {
}
async show() {
this.user = await Auth.GetUser();
this.editModal = document.createElement("div");
this.editModal.classList.add("modal", "is-active");
this.editModal.classList.add("animate__animated", "animate__fadeIn");
......@@ -48,7 +47,7 @@ export class Edit {
this.save();
});
this.gid("edit-share").addEventListener("click", () => {
const shareModal = new Share(this.hostname, this.file);
const shareModal = new Share(this.user, this.hostname, this.file);
shareModal.show();
});
}
......
......@@ -212,7 +212,7 @@ export class Explorer {
HandleError(e);
}
} else if (GetType(file)) {
const openModal = new Open(this.hostname, this.fullHostname, this.files, file);
const openModal = new Open(this.user, this.hostname, this.fullHostname, this.files, file);
openModal.show(true);
}
});
......@@ -233,7 +233,7 @@ export class Explorer {
if (GetType(file) === "text") {
document.getElementById(`file-${file.id}-edit`).addEventListener("click", (event) => {
event.stopPropagation();
const editModal = new Edit(this.fullHostname, file);
const editModal = new Edit(this.user, this.fullHostname, file);
editModal.show(true);
});
}
......@@ -250,7 +250,7 @@ export class Explorer {
}
document.getElementById(`file-${file.id}-share`).addEventListener("click", (event) => {
event.stopPropagation();
const shareModal = new Share(this.hostname, file);
const shareModal = new Share(this.user, this.hostname, file);
shareModal.show(true);
});
}
......
// Imports
import { AnimateCSS, RandomString, GetType, GID } from "/services/common/common.js";
import { Share } from "/components/davs/share.js";
import * as Auth from "/services/auth/auth.js";
import { HandleError } from "/services/common/errors.js";
export class Open {
constructor(hostname, fullHostname, files, file) {
constructor(user, hostname, fullHostname, files, file) {
this.user = user;
this.hostname = hostname;
this.fullHostname = fullHostname;
this.files = files;
......@@ -40,7 +40,7 @@ export class Open {
this.seek(true);
});
this.gid("open-share").addEventListener("click", () => {
const shareModal = new Share(this.hostname, this.file);
const shareModal = new Share(this.user, this.hostname, this.file);
shareModal.show();
});
// Display
......@@ -62,7 +62,6 @@ export class Open {
}
async showContent() {
this.user = await Auth.GetUser();
let content;
let token;
if (this.type == "text") {
......
// Imports
import { AnimateCSS, RandomString, GID } from "/services/common/common.js";
import * as Auth from "/services/auth/auth.js";
import { HandleError } from "/services/common/errors.js";
export class Share {
constructor(hostname, file) {
constructor(user, hostname, file) {
this.user = user;
this.hostname = hostname;
this.file = file;
this.url = `${hostname}${file.path}`;
......@@ -18,7 +18,6 @@ export class Share {
}
async show() {
this.user = await Auth.GetUser();
let shareModal = document.createElement("div");
shareModal.classList.add("modal", "animate__animated", "animate__fadeIn", "is-active");
shareModal.innerHTML = /* HTML */ `
......
// Imports
import * as Navbar from "/components/navbar/navbar.js";
import { loginModes } from "/assets/brand/brand.js";
import * as Auth from "/services/auth/auth.js";
import { HandleError } from "/services/common/errors.js";
import { IsEmpty } from "/services/common/common.js";
// DOM elements
let mountpoint;
let login_field;
let password_field;
let login_inmemory;
let login_icon;
export class Login {
constructor(user, navbar) {
this.user = user;
this.navbar = navbar;
}
export async function mount(where) {
mountpoint = where;
let user = await Auth.GetUser();
if (user !== undefined) {
document.getElementById(mountpoint).innerHTML = "";
location.hash = "#";
} else {
Navbar.CreateMenu();
document.getElementById(mountpoint).innerHTML = /* HTML */ `
<div class="columns">
<div class="column is-half is-offset-one-quarter">
<div class="card">
<div class="card-content">
<div class="field">
<p class="control has-icons-left has-icons-right">
<input id="login-login" class="input" type="text" placeholder="Login" />
<span class="icon is-small is-left">
<i class="fas fa-user"></i>
</span>
</p>
</div>
<div class="field">
<p class="control has-icons-left">
<input id="login-password" class="input" type="password" placeholder="Password" />
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
</span>
</p>
// DOM elements
login_field;
password_field;
login_inmemory;
login_icon;
async mount(mountpoint) {
if (!IsEmpty(this.user)) {
document.getElementById(mountpoint).innerHTML = "";
location.hash = "#";
} else {
this.navbar.CreateMenu();
document.getElementById(mountpoint).innerHTML = /* HTML */ `
<div class="columns">
<div class="column is-half is-offset-one-quarter">
<div class="card">
<div class="card-content">
<div class="field">
<p class="control has-icons-left has-icons-right">
<input id="login-login" class="input" type="text" placeholder="Login" />
<span class="icon is-small is-left">
<i class="fas fa-user"></i>
</span>
</p>
</div>
<div class="field">
<p class="control has-icons-left">
<input id="login-password" class="input" type="password" placeholder="Password" />
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
</span>
</p>
</div>
</div>
<footer class="card-footer">
${loginModes.inmemory
? /* HTML */ `
<a id="login-inmemory" class="card-footer-item">
<span class="icon" id="login-icon"><i class="fas fa-key"></i></span>Login
</a>
`
: ""}
${loginModes.oauth2
? /* HTML */ `
<a id="login-oauth2" class="card-footer-item" href="/OAuth2Login">
<span class="icon"><i class="fab fa-keycdn"></i></span>Login with OAuth2
</a>
`
: ""}
</footer>
</div>
<footer class="card-footer">
${loginModes.inmemory
? /* HTML */ `
<a id="login-inmemory" class="card-footer-item">
<span class="icon" id="login-icon"><i class="fas fa-key"></i></span>Login
</a>
`
: ""}
${loginModes.oauth2
? /* HTML */ `
<a id="login-oauth2" class="card-footer-item" href="/OAuth2Login">
<span class="icon"><i class="fab fa-keycdn"></i></span>Login with OAuth2
</a>
`
: ""}
</footer>
</div>
</div>
</div>
`;
registerModalFields();
}
}
function registerModalFields() {
login_field = document.getElementById("login-login");
password_field = document.getElementById("login-password");
password_field.addEventListener("keyup", function (event) {
// Number 13 is the "Enter" key on the keyboard
if (event.keyCode === 13) {
doLogin();
`;
this.registerModalFields();
}
});
login_inmemory = document.getElementById("login-inmemory");
login_inmemory.addEventListener("click", function () {
doLogin();
});
login_icon = document.getElementById("login-icon");
}
}
async function doLogin() {
login_icon.classList.add("fa-pulse");
try {
const response = await fetch("/Login", {
method: "POST",
body: JSON.stringify({
login: login_field.value,
password: password_field.value,
}),
registerModalFields() {
this.login_field = document.getElementById("login-login");
this.password_field = document.getElementById("login-password");
this.password_field.addEventListener("keyup", (event) => {
// Number 13 is the "Enter" key on the keyboard
if (event.keyCode === 13) {
this.doLogin();
}
});
this.login_inmemory = document.getElementById("login-inmemory");
this.login_inmemory.addEventListener("click", () => {
this.doLogin();
});
if (response.status !== 200) {
throw new Error(`Login error (status ${response.status})`);
this.login_icon = document.getElementById("login-icon");
}
async doLogin() {
this.login_icon.classList.add("fa-pulse");
try {
const response = await fetch("/Login", {
method: "POST",
body: JSON.stringify({
login: this.login_field.value,
password: this.password_field.value,
}),
});
if (response.status !== 200) {
throw new Error(`Login error (status ${response.status})`);
}
const newUser = await Auth.GetUser();
Object.assign(this.user, newUser);
location.hash = "#davs";
this.navbar.CreateMenu();
} catch (e) {
HandleError(e);
this.login_icon.classList.remove("fa-pulse");
}
await Auth.GetUser();
location.hash = "#davs";
Navbar.CreateMenu();
} catch (e) {
HandleError(e);
login_icon.classList.remove("fa-pulse");
}
}
// Imports
import * as Auth from "/services/auth/auth.js";
import * as brand from "/assets/brand/brand.js";
import { AnimateCSS } from "/services/common/common.js";
import { AnimateCSS, IsEmpty } from "/services/common/common.js";
// local variables
let user;
let menu;
export class Navbar {
constructor(user) {
this.user = user;
}
// local variables
user;
menu;
export function mount(mountpoint) {
const where = document.getElementById(mountpoint);
window.document.title = brand.windowTitle;
where.innerHTML = /* HTML */ `
<div class="navbar-brand">
<a class="navbar-item is-size-4" href="/"><img src="assets/brand/logo.svg" alt="logo" />${brand.navTitle}</a>
<a role="button" id="navbar-burger" class="navbar-burger burger" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbar-menu" class="navbar-menu"></div>
`;
// Hamburger menu
const burger = document.getElementById("navbar-burger");
menu = document.getElementById("navbar-menu");
const openClose = async (e) => {
if (burger.classList.contains("is-active")) {
await AnimateCSS(menu, "slideOutRight");
menu.classList.remove("is-active");
burger.classList.remove("is-active");
} else {
if (e.srcElement == burger) {
menu.classList.add("is-active");
burger.classList.add("is-active");
AnimateCSS(menu, "slideInRight");
mount(mountpoint) {
const where = document.getElementById(mountpoint);
window.document.title = brand.windowTitle;
where.innerHTML = /* HTML */ `
<div class="navbar-brand">
<a class="navbar-item is-size-4" href="/"><img src="assets/brand/logo.svg" alt="logo" />${brand.navTitle}</a>
<a role="button" id="navbar-burger" class="navbar-burger burger" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbar-menu" class="navbar-menu"></div>
`;
// Hamburger menu
const burger = document.getElementById("navbar-burger");
this.menu = document.getElementById("navbar-menu");
const openClose = async (e) => {
if (burger.classList.contains("is-active")) {
await AnimateCSS(this.menu, "slideOutRight");
this.menu.classList.remove("is-active");
burger.classList.remove("is-active");
} else {
if (e.srcElement == burger || e.srcElement.offsetParent == burger) {
this.menu.classList.add("is-active");
burger.classList.add("is-active");
AnimateCSS(this.menu, "slideInRight");
}
}
}
};
burger.addEventListener("click", openClose);
menu.addEventListener("click", openClose);
CreateMenu();
}
};
burger.addEventListener("click", openClose);
this.menu.addEventListener("click", openClose);
this.CreateMenu();
}
export async function CreateMenu() {
user = await Auth.GetUser();
menu.innerHTML = /* HTML */ `
<div class="navbar-start">
${user === undefined
? ``
: /* HTML */ `
<a id="navbar-apps" class="navbar-item" href="#apps"><i class="navbar-menu-icon fas fa-home"></i>Apps</a>
<a id="navbar-davs" class="navbar-item" href="#davs"><i class="navbar-menu-icon fas fa-folder-open"></i>Files</a>
${user.isAdmin
? /* HTML */ `
<a id="navbar-users" class="navbar-item" href="#users"><i class="navbar-menu-icon fas fa-users"></i>Users</a>
<a id="navbar-sysinfo" class="navbar-item" href="#sysinfo"><i class="navbar-menu-icon fas fa-stethoscope"></i>System information</a>
`
: ""}
`}
</div>
<div class="navbar-end">
${user === undefined
? /* HTML */ ` <a class="navbar-item" href="#login"><i class="navbar-menu-icon fas fa-sign-in-alt"></i>Log in</a> `
: /* HTML */ ` <a class="navbar-item" href="/Logout"><i class="navbar-menu-icon fas fa-sign-out-alt"></i>Log out</a> `}
</div>
`;
SetActiveItem();
}
async CreateMenu() {
this.menu.innerHTML = /* HTML */ `
<div class="navbar-start">
${IsEmpty(this.user)
? ``
: /* HTML */ `
<a id="navbar-apps" class="navbar-item" href="#apps"><i class="navbar-menu-icon fas fa-home"></i>Apps</a>
<a id="navbar-davs" class="navbar-item" href="#davs"><i class="navbar-menu-icon fas fa-folder-open"></i>Files</a>
${this.user.isAdmin
? /* HTML */ `
<a id="navbar-users" class="navbar-item" href="#users"><i class="navbar-menu-icon fas fa-users"></i>Users</a>
<a id="navbar-sysinfo" class="navbar-item" href="#sysinfo"><i class="navbar-menu-icon fas fa-stethoscope"></i>System information</a>
`
: ""}
`}
</div>
<div class="navbar-end">
${IsEmpty(this.user)
? /* HTML */ ` <a class="navbar-item" href="#login"><i class="navbar-menu-icon fas fa-sign-in-alt"></i>Log in</a> `
: /* HTML */ ` <a class="navbar-item" href="/Logout"><i class="navbar-menu-icon fas fa-sign-out-alt"></i>Log out</a> `}
</div>
`;
this.SetActiveItem();
}
export function SetActiveItem() {