WIP: new user management and authentication system, use go 1.16 embed

This commit is contained in:
Christoph Haas 2021-02-24 21:24:45 +01:00
parent 43bab58f0a
commit 9b10d099b6
40 changed files with 2161 additions and 953 deletions

View File

@ -4,7 +4,7 @@
######- ######-
# Start from the latest golang base image as builder image (only used to compile the code) # Start from the latest golang base image as builder image (only used to compile the code)
######- ######-
FROM golang:1.15 as builder FROM golang:1.16 as builder
RUN mkdir /build RUN mkdir /build
@ -29,7 +29,7 @@ FROM debian:buster
ENV TZ=Europe/Vienna ENV TZ=Europe/Vienna
# GOSS for container health checks # GOSS for container health checks
ENV GOSS_VERSION v0.3.14 ENV GOSS_VERSION v0.3.16
RUN apt-get update && apt-get upgrade -y && \ RUN apt-get update && apt-get upgrade -y && \
apt-get install --no-install-recommends -y moreutils ca-certificates curl && \ apt-get install --no-install-recommends -y moreutils ca-certificates curl && \
rm -rf /var/cache/apt /var/lib/apt/lists/*; \ rm -rf /var/cache/apt /var/lib/apt/lists/*; \

View File

@ -11,12 +11,10 @@ IMAGE=h44z/wg-portal
all: dep build all: dep build
build: dep $(addsuffix -amd64,$(addprefix $(BUILDDIR)/,$(BINARIES))) build: dep $(addsuffix -amd64,$(addprefix $(BUILDDIR)/,$(BINARIES)))
cp -r assets $(BUILDDIR)
cp scripts/wg-portal.service $(BUILDDIR) cp scripts/wg-portal.service $(BUILDDIR)
cp scripts/wg-portal.env $(BUILDDIR) cp scripts/wg-portal.env $(BUILDDIR)
build-cross-plat: dep build $(addsuffix -arm,$(addprefix $(BUILDDIR)/,$(BINARIES))) $(addsuffix -arm64,$(addprefix $(BUILDDIR)/,$(BINARIES))) build-cross-plat: dep build $(addsuffix -arm,$(addprefix $(BUILDDIR)/,$(BINARIES))) $(addsuffix -arm64,$(addprefix $(BUILDDIR)/,$(BINARIES)))
cp -r assets $(BUILDDIR)
cp scripts/wg-portal.service $(BUILDDIR) cp scripts/wg-portal.service $(BUILDDIR)
cp scripts/wg-portal.env $(BUILDDIR) cp scripts/wg-portal.env $(BUILDDIR)

View File

@ -10,7 +10,7 @@ use the following instructions:
### Building ### Building
This section describes how to build the WireGuard Portal code. This section describes how to build the WireGuard Portal code.
To compile the final binary, use the Makefile provided in the repository. To compile the final binary, use the Makefile provided in the repository.
As WireGuard Portal is written in Go, **golang >= 1.14** must be installed prior to building. As WireGuard Portal is written in Go, **golang >= 1.16** must be installed prior to building.
``` ```
make build-cross-plat make build-cross-plat

View File

@ -7,14 +7,13 @@
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal)
[![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](https://hub.docker.com/r/h44z/wg-portal/) [![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](https://hub.docker.com/r/h44z/wg-portal/)
A simple web base configuration portal for [WireGuard](https://wireguard.com). A simple, web based configuration portal for [WireGuard](https://wireguard.com).
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage the VPN The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage the VPN
interface. This allows for seamless activation or deactivation of new users, without disturbing existing VPN interface. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
connections. connections.
The configuration portal is designed to use LDAP (Active Directory) as a user source for authentication and profile data. The configuration portal currently supports using SQLite, MySQL as a user source for authentication and profile data.
It still can be used without LDAP by using a predefined administrator account. Some features like mass creation of accounts It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
will only be available in combination with LDAP.
## Features ## Features
* Self-hosted and web based * Self-hosted and web based
@ -24,18 +23,19 @@ will only be available in combination with LDAP.
* Enable / Disable clients seamlessly * Enable / Disable clients seamlessly
* Generation of `wgX.conf` after any modification * Generation of `wgX.conf` after any modification
* IPv6 ready * IPv6 ready
* User authentication (LDAP and/or predefined admin account) * User authentication (SQLite/MySQL and LDAP)
* Dockerized * Dockerized
* Responsive template * Responsive template
* One single binary
![Screenshot](screenshot.png) ![Screenshot](screenshot.png)
## Setup ## Setup
### Docker ### Docker
The easiest way to run WireGuard Portal is using the provided docker image. The easiest way to run WireGuard Portal is to use the Docker image provided.
Docker compose snippet with sample values: Docker Compose snippet with some sample configuration values:
``` ```
version: '3.6' version: '3.6'
services: services:
@ -56,19 +56,20 @@ services:
- WEBSITE_TITLE=WireGuard VPN - WEBSITE_TITLE=WireGuard VPN
- COMPANY_NAME=Your Company Name - COMPANY_NAME=Your Company Name
- MAIL_FROM=WireGuard VPN <noreply+wireguard@company.com> - MAIL_FROM=WireGuard VPN <noreply+wireguard@company.com>
- ADMIN_USER=admin # optional admin user - ADMIN_USER=admin@domain.com
- ADMIN_PASS=supersecret - ADMIN_PASS=supersecret
- ADMIN_LDAP_GROUP=CN=WireGuardAdmins,OU=Users,DC=COMPANY,DC=LOCAL
- EMAIL_HOST=10.10.10.10 - EMAIL_HOST=10.10.10.10
- EMAIL_PORT=25 - EMAIL_PORT=25
- LDAP_ENABLED=true
- LDAP_URL=ldap://srv-ad01.company.local:389 - LDAP_URL=ldap://srv-ad01.company.local:389
- LDAP_BASEDN=DC=COMPANY,DC=LOCAL - LDAP_BASEDN=DC=COMPANY,DC=LOCAL
- LDAP_USER=ldap_wireguard@company.local - LDAP_USER=ldap_wireguard@company.local
- LDAP_PASSWORD=supersecretldappassword - LDAP_PASSWORD=supersecretldappassword
- LDAP_ADMIN_GROUP=CN=WireGuardAdmins,OU=Users,DC=COMPANY,DC=LOCAL
``` ```
Please note that mapping ```/etc/wireguard``` to ```/etc/wireguard``` inside the docker, will erase your host's current configuration. Please note that mapping ```/etc/wireguard``` to ```/etc/wireguard``` inside the docker, will erase your host's current configuration.
If needed, please make sure to backup your files from ```/etc/wireguard```. If needed, please make sure to backup your files from ```/etc/wireguard```.
For a full list of configuration options take a look at the source file [internal/common/configuration.go](internal/common/configuration.go). For a full list of configuration options take a look at the source file [internal/common/configuration.go](internal/common/configuration.go#L57).
### Standalone ### Standalone
For a standalone application, use the Makefile provided in the repository to build the application. For a standalone application, use the Makefile provided in the repository to build the application.
@ -80,7 +81,7 @@ make
make build-cross-plat make build-cross-plat
``` ```
The compiled binary and all necessary assets will be located in the dist folder. The compiled binary will be located in the dist folder.
A detailed description for using this software with a raspberry pi can be found in the [README-RASPBERRYPI.md](README-RASPBERRYPI.md). A detailed description for using this software with a raspberry pi can be found in the [README-RASPBERRYPI.md](README-RASPBERRYPI.md).
## What is out of scope ## What is out of scope

View File

@ -40,6 +40,16 @@ pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768
/* -------------------------------------------------- /* --------------------------------------------------
End collapsable table*/ End collapsable table*/
.jumbotron-home {
padding: 1rem 1rem;
}
@media (min-width: 576px) {
.jumbotron-home {
padding: 2rem 2rem;
}
}
@media (min-width: 1440px) { @media (min-width: 1440px) {
.container, .container-lg, .container-md, .container-sm, .container-xl { .container, .container-lg, .container-md, .container-sm, .container-xl {
max-width: 1400px; max-width: 1400px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
assets/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -53,7 +53,7 @@
} }
}).tokenfield({ }).tokenfield({
autocomplete: { autocomplete: {
source: [{{range $i, $u :=.Users}}{{$u.Mail}},{{end}}], source: [{{range $i, $u :=.Users}}{{$u.Email}},{{end}}],
delay: 100 delay: 100
}, },
showAutocompleteOnFocus: false showAutocompleteOnFocus: false

View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Users</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
{{if eq .User.CreatedAt .Epoch}}
<h1>Create a new user</h1>
{{else}}
<h1>Edit user <strong>{{.User.Email}}</strong></h1>
{{end}}
{{template "prt_flashes.html" .}}
<form method="post" enctype="multipart/form-data">
{{if eq .User.CreatedAt .Epoch}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputEmail">Email</label>
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.User.Email}}">
</div>
</div>
{{else}}
<input type="hidden" name="email" value="{{.User.Email}}">
{{end}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputFirstname">Firstname</label>
<input type="text" name="firstname" class="form-control" id="inputFirstname" value="{{.User.Firstname}}">
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputLastname">Lastname</label>
<input type="text" name="lastname" class="form-control" id="inputLastname" value="{{.User.Lastname}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="inputPhone">Phone</label>
<input type="text" name="phone" class="form-control" id="inputPhone" value="{{.User.Phone}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12 {{if eq .User.CreatedAt .Epoch}}required{{end}}">
<label for="inputPassword">Password</label>
<input type="password" name="password" class="form-control" id="inputPassword">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isadmin" type="checkbox" value="true" id="inputAdmin" {{if .User.IsAdmin}}checked{{end}}>
<label class="custom-control-label" for="inputAdmin">
Administrator
</label>
</div>
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="inputDisabled" {{if .User.DeletedAt.Valid}}checked{{end}}>
<label class="custom-control-label" for="inputDisabled">
Disabled
</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin/users/" class="btn btn-secondary">Cancel</a>
</form>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@ -84,13 +84,11 @@
</div> </div>
<div class="mt-4 row"> <div class="mt-4 row">
<div class="col-sm-10 col-12"> <div class="col-sm-10 col-12">
<h2 class="mt-2">Current VPN Users</h2> <h2 class="mt-2">Current VPN Peers</h2>
</div> </div>
<div class="col-sm-2 col-12 text-right"> <div class="col-sm-2 col-12 text-right">
{{if not .Static.LdapDisabled}} <a href="/admin/peer/createldap" title="Add multiple peers" class="btn btn-primary"><i class="fa fa-fw fa-user-plus"></i></a>
<a href="/admin/peer/createldap" title="Add LDAP users" class="btn btn-primary"><i class="fa fa-fw fa-user-plus"></i></a> <a href="/admin/peer/create" title="Manually add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>M</a>
{{end}}
<a href="/admin/peer/create" title="Manually add a user" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>M</a>
</div> </div>
</div> </div>
<div class="mt-2 table-responsive"> <div class="mt-2 table-responsive">
@ -98,11 +96,11 @@
<thead> <thead>
<tr> <tr>
<th scope="col" class="list-image-cell"></th><!-- Status and expand --> <th scope="col" class="list-image-cell"></th><!-- Status and expand -->
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "id"}}"></i></a></th> <th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "peers" "id"}}"></i></a></th>
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "pubKey"}}"></i></a></th> <th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "peers" "pubKey"}}"></i></a></th>
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "mail"}}"></i></a></th> <th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "peers" "mail"}}"></i></a></th>
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "ip"}}"></i></a></th> <th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "peers" "ip"}}"></i></a></th>
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "handshake"}}"></i></a></th> <th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "peers" "handshake"}}"></i></a></th>
<th scope="col"></th><!-- Actions --> <th scope="col"></th><!-- Actions -->
</tr> </tr>
</thead> </thead>
@ -144,15 +142,14 @@
<div class="tab-content" id="tabContent{{$p.UID}}"> <div class="tab-content" id="tabContent{{$p.UID}}">
<div id="t1{{$p.UID}}" class="tab-pane fade active show"> <div id="t1{{$p.UID}}" class="tab-pane fade active show">
<h4>User details</h4> <h4>User details</h4>
{{if not $p.LdapUser}} {{if not $p.User}}
<p>No LDAP user-information available...</p> <p>No user information available...</p>
{{else}} {{else}}
<ul> <ul>
<li>Firstname: {{$p.LdapUser.Firstname}}</li> <li>Firstname: {{$p.User.Firstname}}</li>
<li>Lastname: {{$p.LdapUser.Lastname}}</li> <li>Lastname: {{$p.User.Lastname}}</li>
<li>Phone: {{index $p.LdapUser.RawLdapData.Attributes "telephoneNumber"}}</li> <li>Phone: {{$p.User.Phone}}</li>
<li>Mail: {{$p.LdapUser.Mail}}</li> <li>Mail: {{$p.User.Email}}</li>
<li>Department: {{index $p.LdapUser.RawLdapData.Attributes "department"}}</li>
</ul> </ul>
{{end}} {{end}}
<h4>Connection / Traffic</h4> <h4>Connection / Traffic</h4>

View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Users</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
<h1>WireGuard VPN Users</h1>
{{template "prt_flashes.html" .}}
<div class="mt-4 row">
<div class="col-sm-10 col-12">
<h2 class="mt-2">All Users</h2>
</div>
<div class="col-sm-2 col-12 text-right">
<a href="/admin/users/create" title="Add a user" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>M</a>
</div>
</div>
<div class="mt-2 table-responsive">
<table class="table table-sm" id="userTable">
<thead>
<tr>
<th scope="col"><a href="?sort=email">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "users" "email"}}"></i></a></th>
<th scope="col"><a href="?sort=lastname">Lastname <i class="fa fa-fw {{.Session.GetSortIcon "users" "lastname"}}"></i></a></th>
<th scope="col"><a href="?sort=firstname">Firstname <i class="fa fa-fw {{.Session.GetSortIcon "users" "firstname"}}"></i></a></th>
<th scope="col"><a href="?sort=source">Source <i class="fa fa-fw {{.Session.GetSortIcon "users" "source"}}"></i></a></th>
<th scope="col"><a href="?sort=admin">Is Admin <i class="fa fa-fw {{.Session.GetSortIcon "users" "admin"}}"></i></a></th>
<th scope="col"></th><!-- Actions -->
</tr>
</thead>
<tbody>
{{range $i, $u :=.Users}}
<tr id="user-pos-{{$i}}" {{if $u.DeletedAt.Valid}}class="disabled-peer"{{end}}>
<td>{{$u.Email}}</td>
<td>{{$u.Lastname}}</td>
<td>{{$u.Firstname}}</td>
<td>{{$u.Source}}</td>
<td>{{if $u.IsAdmin}}True{{else}}False{{end}}</td>
<td>
{{if eq $.Session.IsAdmin true}}
{{if eq $u.Source "db"}}
<a href="/admin/users/edit?pkey={{$u.Email}}" title="Edit user"><i class="fas fa-cog"></i></a>
{{end}}
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
<p>Currently listed users: <strong>{{len .Users}}</strong></p>
</div>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@ -13,18 +13,69 @@
<body id="page-top" class="d-flex flex-column min-vh-100"> <body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}} {{template "prt_nav.html" .}}
<div class="container mt-5"> <div class="container mt-2">
<div class="page-header"> <div class="page-header">
<h1>WireGuard VPN Portal</h1> <h1>WireGuard VPN Portal</h1>
</div> </div>
{{template "prt_flashes.html" .}} {{template "prt_flashes.html" .}}
<p class="lead">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN. </p> <p class="lead">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN. </p>
<h3 class="mt-3">More Information</h3>
<div class="row">
<div class="col-lg-4">
<div class="card border-secondary mb-4" style="min-height: 15rem;">
<div class="card-header">WireGuard Installation</div>
<div class="card-body">
<h4 class="card-title">Installation</h4>
<p class="card-text">Installation instructions for client software can be found on the official WireGuard website.</p>
<a href="https://www.wireguard.com/install/" title="WireGuard Installation" target="_blank" class="btn btn-primary btn-sm">Open Instructions</a>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-secondary mb-4" style="min-height: 15rem;">
<div class="card-header">About WireGuard</div>
<div class="card-body">
<h4 class="card-title">About</h4>
<p class="card-text">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography.</p>
<a href="https://www.wireguard.com/" title="WireGuard" target="_blank" class="btn btn-primary btn-sm">More details</a>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-secondary mb-4" style="min-height: 15rem;">
<div class="card-header">About WireGuard Portal</div>
<div class="card-body">
<h4 class="card-title">WireGuard Portal</h4>
<p class="card-text">WireGuard Portal is a simple, web based configuration portal for WireGuard.</p>
<a href="https://github.com/h44z/wg-portal/" title="WireGuard Portal" target="_blank" class="btn btn-primary btn-sm">More details</a>
</div>
</div>
</div>
</div>
<h3>VPN Profiles and configuration</h3> <div class="jumbotron jumbotron-home">
<p>You can access your personal VPN configurations via your Userprofile: <a href="/user/profile" class="btn btn-primary" title="User-Profile">Open Userprofile</a></p> <h2 class="display-5">VPN Profiles</h2>
<p class="lead">You can access and download your personal VPN configurations via your Userprofile.</p>
<hr class="my-4">
<p>To find all your configured profiles click on the button below.</p>
<p class="lead">
<a href="/user/profile" class="btn btn-primary btn-lg" title="User-Profile">Open My Profile</a>
</p>
</div>
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
<div class="jumbotron jumbotron-home">
<h2 class="display-5">Administration Area</h2>
<p class="lead">In the administration area you can manage WireGuard peers and the server interface as well as users that are allowed to log in to the WireGuard Portal.</p>
<hr class="my-4">
<p>To find all your configured profiles click on the button below.</p>
<p class="lead">
<a href="/admin/" class="btn btn-primary btn-lg" title="WireGuard Administration">Open WireGuard Administration</a>
<a href="/admin/users/" class="btn btn-primary btn-lg" title="User Administration">Open User Administration</a>
</p>
</div>
{{end}}{{end}}
<h3>Client Software</h3>
<p>Installation instructions for client software can be found on the official WireGuard website: <a href="https://www.wireguard.com/install/" title="WireGuard" target="_blank">https://www.wireguard.com/</a> </p>
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>

View File

@ -7,19 +7,28 @@
<div id="topNavbar" class="navbar-collapse collapse"> <div id="topNavbar" class="navbar-collapse collapse">
<ul class="navbar-nav mr-auto mt-2 mt-lg-0"> <ul class="navbar-nav mr-auto mt-2 mt-lg-0">
<li class="nav-spacer"></li> <li class="nav-spacer"></li>
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}{{with eq $.Route "/admin/"}} {{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
{{with eq $.Route "/admin/"}}
<form class="form-inline my-2 my-lg-0" method="get"> <form class="form-inline my-2 my-lg-0" method="get">
<input class="form-control mr-sm-2" name="search" type="search" placeholder="Search" aria-label="Search" value="{{$.Session.Search}}"> <input class="form-control mr-sm-2" name="search" type="search" placeholder="Search" aria-label="Search" value="{{index $.Session.Search "peers"}}">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit"><i class="fa fa-search"></i></button> <button class="btn btn-outline-success my-2 my-sm-0" type="submit"><i class="fa fa-search"></i></button>
</form> </form>
{{end}}{{end}}{{end}} {{end}}
{{with eq $.Route "/admin/users/"}}
<form class="form-inline my-2 my-lg-0" method="get">
<input class="form-control mr-sm-2" name="search" type="search" placeholder="Search" aria-label="Search" value="{{index $.Session.Search "users"}}">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit"><i class="fa fa-search"></i></button>
</form>
{{end}}
{{end}}{{end}}
</ul> </ul>
{{if eq $.Session.LoggedIn true}} {{if eq $.Session.LoggedIn true}}
<div class="nav-item dropdown"> <div class="nav-item dropdown">
<a href="#" class="navbar-text dropdown-toggle" data-toggle="dropdown">{{$.Session.Firstname}} {{$.Session.Lastname}} <span class="caret"></span></a> <a href="#" class="navbar-text dropdown-toggle" data-toggle="dropdown">{{$.Session.Firstname}} {{$.Session.Lastname}} <span class="caret"></span></a>
<div class="dropdown-menu"> <div class="dropdown-menu">
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}} {{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
<a class="dropdown-item" href="/admin/"><i class="fas fa-file-export"></i> Administration</a> <a class="dropdown-item" href="/admin/"><i class="fas fa-cogs"></i> Administration</a>
<a class="dropdown-item" href="/admin/users/"><i class="fas fa-users-cog"></i> User Management</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
{{end}}{{end}} {{end}}{{end}}
<a class="dropdown-item" href="/user/profile"><i class="fas fa-user"></i> Profile</a> <a class="dropdown-item" href="/user/profile"><i class="fas fa-user"></i> Profile</a>

View File

@ -21,11 +21,11 @@
<thead> <thead>
<tr> <tr>
<th scope="col" class="list-image-cell"></th><!-- Status and expand --> <th scope="col" class="list-image-cell"></th><!-- Status and expand -->
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "id"}}"></i></a></th> <th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "id"}}"></i></a></th>
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "pubKey"}}"></i></a></th> <th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "pubKey"}}"></i></a></th>
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "mail"}}"></i></a></th> <th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "mail"}}"></i></a></th>
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "ip"}}"></i></a></th> <th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "ip"}}"></i></a></th>
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "handshake"}}"></i></a></th> <th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "handshake"}}"></i></a></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -58,15 +58,14 @@
<div class="tab-content" id="tabContent{{$p.UID}}"> <div class="tab-content" id="tabContent{{$p.UID}}">
<div id="t1{{$p.UID}}" class="tab-pane fade active show"> <div id="t1{{$p.UID}}" class="tab-pane fade active show">
<h4>User details</h4> <h4>User details</h4>
{{if not $p.LdapUser}} {{if not $p.User}}
<p>No LDAP user-information available...</p> <p>No user information available...</p>
{{else}} {{else}}
<ul> <ul>
<li>Firstname: {{$p.LdapUser.Firstname}}</li> <li>Firstname: {{$p.User.Firstname}}</li>
<li>Lastname: {{$p.LdapUser.Lastname}}</li> <li>Lastname: {{$p.User.Lastname}}</li>
<li>Phone: {{$p.UID}}</li> <li>Phone: {{$p.User.Phone}}</li>
<li>Mail: {{$p.LdapUser.Mail}}</li> <li>Mail: {{$p.User.Email}}</li>
<li>Department: {{$p.UID}}</li>
</ul> </ul>
{{end}} {{end}}
<h4>Traffic</h4> <h4>Traffic</h4>

12
efs.go Normal file
View File

@ -0,0 +1,12 @@
package wg_portal
import "embed"
//go:embed assets/tpl/*
var Templates embed.FS
//go:embed assets/css/*
//go:embed assets/fonts/*
//go:embed assets/img/*
//go:embed assets/js/*
var Statics embed.FS

7
go.mod
View File

@ -1,6 +1,6 @@
module github.com/h44z/wg-portal module github.com/h44z/wg-portal
go 1.14 go 1.16
require ( require (
github.com/gin-contrib/sessions v0.0.3 github.com/gin-contrib/sessions v0.0.3
@ -11,12 +11,15 @@ require (
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/milosgajdos/tenus v0.0.3 github.com/milosgajdos/tenus v0.0.3
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.7.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
github.com/toorop/gin-logrus v0.0.0-20200831135515-d2ee50d38dae github.com/toorop/gin-logrus v0.0.0-20200831135515-d2ee50d38dae
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
gorm.io/driver/mysql v1.0.4
gorm.io/driver/sqlite v1.1.3 gorm.io/driver/sqlite v1.1.3
gorm.io/gorm v1.20.5 gorm.io/gorm v1.20.12
) )

View File

@ -0,0 +1,31 @@
package authentication
import (
"github.com/gin-gonic/gin"
)
type AuthContext struct {
Provider AuthProvider
Username string // email or username
Password string // optional for OIDC
Callback string // callback for OIDC
}
type AuthProviderType string
const (
AuthProviderTypePassword AuthProviderType = "password"
AuthProviderTypeOauth AuthProviderType = "oauth"
)
type AuthProvider interface {
GetName() string
GetType() AuthProviderType
GetPriority() int // lower number = higher priority
Login(*AuthContext) (string, error)
Logout(*AuthContext) error
GetUserModel(*AuthContext) (*User, error)
SetupRoutes(routes *gin.RouterGroup)
}

View File

@ -0,0 +1,203 @@
package ldap
import (
"crypto/tls"
"fmt"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-ldap/ldap/v3"
"github.com/h44z/wg-portal/internal/authentication"
ldapconfig "github.com/h44z/wg-portal/internal/ldap"
"github.com/h44z/wg-portal/internal/users"
"github.com/pkg/errors"
)
// Provider provide login with password method
type Provider struct {
config *ldapconfig.Config
}
func New(cfg *ldapconfig.Config) (*Provider, error) {
p := &Provider{
config: cfg,
}
// test ldap connectivity
client, err := p.open()
if err != nil {
return nil, errors.Wrap(err, "unable to open ldap connection")
}
defer p.close(client)
return p, nil
}
// GetName return provider name
func (Provider) GetName() string {
return string(users.UserSourceLdap)
}
// GetType return provider type
func (Provider) GetType() authentication.AuthProviderType {
return authentication.AuthProviderTypePassword
}
// GetPriority return provider priority
func (Provider) GetPriority() int {
return 1 // LDAP password provider
}
func (provider Provider) SetupRoutes(routes *gin.RouterGroup) {
// nothing todo here
}
func (provider Provider) Login(ctx *authentication.AuthContext) (string, error) {
username := strings.ToLower(ctx.Username)
password := ctx.Password
// Validate input
if strings.Trim(username, " ") == "" || strings.Trim(password, " ") == "" {
return "", errors.New("empty username or password")
}
client, err := provider.open()
if err != nil {
return "", errors.Wrap(err, "unable to open ldap connection")
}
defer provider.close(client)
// Search for the given username
attrs := []string{"dn", provider.config.EmailAttribute}
if provider.config.DisabledAttribute != "" {
attrs = append(attrs, provider.config.DisabledAttribute)
}
searchRequest := ldap.NewSearchRequest(
provider.config.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=%s)(%s=%s))", provider.config.UserClass, provider.config.EmailAttribute, username),
attrs,
nil,
)
sr, err := client.Search(searchRequest)
if err != nil {
return "", errors.Wrap(err, "unable to find user in ldap")
}
if len(sr.Entries) != 1 {
return "", errors.Wrapf(err, "invalid amount of ldap entries (%d)", len(sr.Entries))
}
userDN := sr.Entries[0].DN
// Check if user is disabled, if so deny login
if provider.config.DisabledAttribute != "" {
uac := sr.Entries[0].GetAttributeValue(provider.config.DisabledAttribute)
switch provider.config.Type {
case ldapconfig.TypeActiveDirectory:
if ldapconfig.IsActiveDirectoryUserDisabled(uac) {
return "", errors.Wrapf(err, "user is disabled")
}
case ldapconfig.TypeOpenLDAP:
if ldapconfig.IsOpenLdapUserDisabled(uac) {
return "", errors.Wrapf(err, "user is disabled")
}
}
}
// Bind as the user to verify their password
err = client.Bind(userDN, password)
if err != nil {
return "", errors.Wrapf(err, "invalid credentials")
}
return sr.Entries[0].GetAttributeValue(provider.config.EmailAttribute), nil
}
func (provider Provider) Logout(context *authentication.AuthContext) error {
return nil // nothing todo here
}
func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authentication.User, error) {
username := strings.ToLower(ctx.Username)
// Validate input
if strings.Trim(username, " ") == "" {
return nil, errors.New("empty username")
}
client, err := provider.open()
if err != nil {
return nil, errors.Wrap(err, "unable to open ldap connection")
}
defer provider.close(client)
// Search for the given username
attrs := []string{"dn", provider.config.EmailAttribute, provider.config.FirstNameAttribute, provider.config.LastNameAttribute,
provider.config.PhoneAttribute, provider.config.GroupMemberAttribute}
if provider.config.DisabledAttribute != "" {
attrs = append(attrs, provider.config.DisabledAttribute)
}
searchRequest := ldap.NewSearchRequest(
provider.config.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=%s)(%s=%s))", provider.config.UserClass, provider.config.EmailAttribute, username),
attrs,
nil,
)
sr, err := client.Search(searchRequest)
if err != nil {
return nil, errors.Wrap(err, "unable to find user in ldap")
}
if len(sr.Entries) != 1 {
return nil, errors.Wrapf(err, "invalid amount of ldap entries (%d)", len(sr.Entries))
}
user := &authentication.User{
Firstname: sr.Entries[0].GetAttributeValue(provider.config.FirstNameAttribute),
Lastname: sr.Entries[0].GetAttributeValue(provider.config.LastNameAttribute),
Email: sr.Entries[0].GetAttributeValue(provider.config.EmailAttribute),
Phone: sr.Entries[0].GetAttributeValue(provider.config.PhoneAttribute),
IsAdmin: false,
}
for _, group := range sr.Entries[0].GetAttributeValues(provider.config.GroupMemberAttribute) {
if group == provider.config.AdminLdapGroup {
user.IsAdmin = true
break
}
}
return user, nil
}
func (provider Provider) open() (*ldap.Conn, error) {
conn, err := ldap.DialURL(provider.config.URL)
if err != nil {
return nil, err
}
if provider.config.StartTLS {
// Reconnect with TLS
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
return nil, err
}
}
err = conn.Bind(provider.config.BindUser, provider.config.BindPass)
if err != nil {
return nil, err
}
return conn, nil
}
func (provider Provider) close(conn *ldap.Conn) {
if conn != nil {
conn.Close()
}
}

View File

@ -0,0 +1,186 @@
package password
import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/authentication"
"github.com/h44z/wg-portal/internal/users"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// Provider provide login with password method
type Provider struct {
db *gorm.DB
}
func New(cfg *users.Config) (*Provider, error) {
p := &Provider{}
var err error
p.db, err = users.GetDatabaseForConfig(cfg)
if err != nil {
return nil, errors.Wrapf(err, "failed to setup authentication database %s", cfg.Database)
}
return p, nil
}
// GetName return provider name
func (Provider) GetName() string {
return string(users.UserSourceDatabase)
}
// GetType return provider type
func (Provider) GetType() authentication.AuthProviderType {
return authentication.AuthProviderTypePassword
}
// GetPriority return provider priority
func (Provider) GetPriority() int {
return 0 // DB password provider = highest prio
}
func (provider Provider) SetupRoutes(routes *gin.RouterGroup) {
// nothing todo here
}
func (provider Provider) Login(ctx *authentication.AuthContext) (string, error) {
username := strings.ToLower(ctx.Username)
password := ctx.Password
// Validate input
if strings.Trim(username, " ") == "" || strings.Trim(password, " ") == "" {
return "", errors.New("empty username or password")
}
// Authenticate agains the users database
user := users.User{}
provider.db.Where("email = ?", username).First(&user)
if user.Email == "" {
return "", errors.New("invalid username")
}
// Compare the stored hashed password, with the hashed version of the password that was received
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return "", errors.New("invalid password")
}
return user.Email, nil
}
func (provider Provider) Logout(context *authentication.AuthContext) error {
return nil // nothing todo here
}
func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authentication.User, error) {
username := strings.ToLower(ctx.Username)
// Validate input
if strings.Trim(username, " ") == "" {
return nil, errors.New("empty username")
}
// Fetch usermodel from users database
user := users.User{}
provider.db.Where("email = ?", username).First(&user)
if user.Email != username {
return nil, errors.New("invalid or disabled username")
}
return &authentication.User{
Email: user.Email,
IsAdmin: user.IsAdmin,
Firstname: user.Firstname,
Lastname: user.Lastname,
Phone: user.Phone,
}, nil
}
func (provider Provider) InitializeAdmin(email, password string) error {
admin := users.User{}
provider.db.Unscoped().Where("email = ?", email).FirstOrInit(&admin)
// newly created admin
if admin.Email != email {
// For security reasons a random admin password will be generated if the default one is still in use!
if password == "wgportal" {
password = generateRandomPassword()
fmt.Println("#############################################")
fmt.Println("Administrator credentials:")
fmt.Println(" Email: ", email)
fmt.Println(" Password: ", password)
fmt.Println()
fmt.Println("This information will only be displayed once!")
fmt.Println("#############################################")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return errors.Wrap(err, "failed to hash admin password")
}
admin.Email = email
admin.Password = string(hashedPassword)
admin.Firstname = "WireGuard"
admin.Lastname = "Administrator"
admin.CreatedAt = time.Now()
admin.UpdatedAt = time.Now()
admin.IsAdmin = true
admin.Source = users.UserSourceDatabase
res := provider.db.Create(admin)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to create admin %s", admin.Email)
}
}
// update/reactivate
if !admin.IsAdmin || admin.DeletedAt.Valid {
// For security reasons a random admin password will be generated if the default one is still in use!
if password == "wgportal" {
password = generateRandomPassword()
fmt.Println("#############################################")
fmt.Println("Administrator credentials:")
fmt.Println(" Email: ", email)
fmt.Println(" Password: ", password)
fmt.Println()
fmt.Println("This information will only be displayed once!")
fmt.Println("#############################################")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return errors.Wrap(err, "failed to hash admin password")
}
admin.Password = string(hashedPassword)
admin.IsAdmin = true
admin.UpdatedAt = time.Now()
res := provider.db.Save(admin)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to update admin %s", admin.Email)
}
}
return nil
}
func generateRandomPassword() string {
rand.Seed(time.Now().Unix())
var randPassword strings.Builder
charSet := "abcdedfghijklmnopqrstABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$"
for i := 0; i < 12; i++ {
random := rand.Intn(len(charSet))
randPassword.WriteString(string(charSet[random]))
}
return randPassword.String()
}

View File

@ -0,0 +1,11 @@
package authentication
type User struct {
Email string
IsAdmin bool
// optional fields
Firstname string
Lastname string
Phone string
}

View File

@ -7,6 +7,7 @@ import (
"runtime" "runtime"
"github.com/h44z/wg-portal/internal/ldap" "github.com/h44z/wg-portal/internal/ldap"
"github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard" "github.com/h44z/wg-portal/internal/wireguard"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -58,18 +59,17 @@ type Config struct {
ExternalUrl string `yaml:"externalUrl" envconfig:"EXTERNAL_URL"` ExternalUrl string `yaml:"externalUrl" envconfig:"EXTERNAL_URL"`
Title string `yaml:"title" envconfig:"WEBSITE_TITLE"` Title string `yaml:"title" envconfig:"WEBSITE_TITLE"`
CompanyName string `yaml:"company" envconfig:"COMPANY_NAME"` CompanyName string `yaml:"company" envconfig:"COMPANY_NAME"`
MailFrom string `yaml:"mailfrom" envconfig:"MAIL_FROM"` MailFrom string `yaml:"mailFrom" envconfig:"MAIL_FROM"`
AdminUser string `yaml:"adminUser" envconfig:"ADMIN_USER"` // optional, non LDAP admin user AdminUser string `yaml:"adminUser" envconfig:"ADMIN_USER"`
AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"` AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"`
DatabasePath string `yaml:"database" envconfig:"DATABASE_PATH"`
EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"` EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"`
CreateInterfaceOnLogin bool `yaml:"createOnLogin" envconfig:"CREATE_INTERFACE_ON_LOGIN"` CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"`
SyncLdapStatus bool `yaml:"syncLdapStatus" envconfig:"SYNC_LDAP_STATUS"` // disable account if disabled in ldap LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
} `yaml:"core"` } `yaml:"core"`
Database users.Config `yaml:"database"`
Email MailConfig `yaml:"email"` Email MailConfig `yaml:"email"`
LDAP ldap.Config `yaml:"ldap"` LDAP ldap.Config `yaml:"ldap"`
WG wireguard.Config `yaml:"wg"` WG wireguard.Config `yaml:"wg"`
AdminLdapGroup string `yaml:"adminLdapGroup" envconfig:"ADMIN_LDAP_GROUP"`
} }
func NewConfig() *Config { func NewConfig() *Config {
@ -81,18 +81,31 @@ func NewConfig() *Config {
cfg.Core.CompanyName = "WireGuard Portal" cfg.Core.CompanyName = "WireGuard Portal"
cfg.Core.ExternalUrl = "http://localhost:8123" cfg.Core.ExternalUrl = "http://localhost:8123"
cfg.Core.MailFrom = "WireGuard VPN <noreply@company.com>" cfg.Core.MailFrom = "WireGuard VPN <noreply@company.com>"
cfg.Core.AdminUser = "" // non-ldap admin access is disabled by default cfg.Core.AdminUser = "admin@wgportal.local"
cfg.Core.AdminPassword = "" cfg.Core.AdminPassword = "wgportal"
cfg.Core.DatabasePath = "data/wg_portal.db" cfg.Core.LdapEnabled = false
cfg.Database.Typ = "sqlite"
cfg.Database.Database = "data/wg_portal.db"
cfg.LDAP.URL = "ldap://srv-ad01.company.local:389" cfg.LDAP.URL = "ldap://srv-ad01.company.local:389"
cfg.LDAP.BaseDN = "DC=COMPANY,DC=LOCAL" cfg.LDAP.BaseDN = "DC=COMPANY,DC=LOCAL"
cfg.LDAP.StartTLS = true cfg.LDAP.StartTLS = true
cfg.LDAP.BindUser = "company\\\\ldap_wireguard" cfg.LDAP.BindUser = "company\\\\ldap_wireguard"
cfg.LDAP.BindPass = "SuperSecret" cfg.LDAP.BindPass = "SuperSecret"
cfg.LDAP.Type = "AD"
cfg.LDAP.UserClass = "organizationalPerson"
cfg.LDAP.EmailAttribute = "mail"
cfg.LDAP.FirstNameAttribute = "givenName"
cfg.LDAP.LastNameAttribute = "sn"
cfg.LDAP.PhoneAttribute = "telephoneNumber"
cfg.LDAP.GroupMemberAttribute = "memberOf"
cfg.LDAP.DisabledAttribute = "userAccountControl"
cfg.LDAP.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL"
cfg.WG.DeviceName = "wg0" cfg.WG.DeviceName = "wg0"
cfg.WG.WireGuardConfig = "/etc/wireguard/wg0.conf" cfg.WG.WireGuardConfig = "/etc/wireguard/wg0.conf"
cfg.WG.ManageIPAddresses = true cfg.WG.ManageIPAddresses = true
cfg.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL"
cfg.Email.Host = "127.0.0.1" cfg.Email.Host = "127.0.0.1"
cfg.Email.Port = 25 cfg.Email.Port = 25

View File

@ -1,94 +0,0 @@
package ldap
import (
"crypto/tls"
"fmt"
"github.com/go-ldap/ldap/v3"
)
type Authentication struct {
Cfg *Config
}
func NewAuthentication(config Config) Authentication {
a := Authentication{
Cfg: &config,
}
return a
}
func (a Authentication) open() (*ldap.Conn, error) {
conn, err := ldap.DialURL(a.Cfg.URL)
if err != nil {
return nil, err
}
if a.Cfg.StartTLS {
// Reconnect with TLS
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
return nil, err
}
}
err = conn.Bind(a.Cfg.BindUser, a.Cfg.BindPass)
if err != nil {
return nil, err
}
return conn, nil
}
func (a Authentication) close(conn *ldap.Conn) {
if conn != nil {
conn.Close()
}
}
func (a Authentication) CheckLogin(username, password string) bool {
return a.CheckCustomLogin("sAMAccountName", username, password)
}
func (a Authentication) CheckCustomLogin(userIdentifier, username, password string) bool {
client, err := a.open()
if err != nil {
return false
}
defer a.close(client)
// Search for the given username
searchRequest := ldap.NewSearchRequest(
a.Cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=organizationalPerson)(%s=%s))", userIdentifier, username),
[]string{"dn", "userAccountControl"},
nil,
)
sr, err := client.Search(searchRequest)
if err != nil {
return false
}
if len(sr.Entries) != 1 {
return false
}
userDN := sr.Entries[0].DN
// Check if user is disabled, if so deny login
uac := sr.Entries[0].GetAttributeValue("userAccountControl")
if uac != "" && IsLdapUserDisabled(uac) {
return false
}
// Bind as the user to verify their password
err = client.Bind(userDN, password)
if err != nil {
return false
}
return true
}

27
internal/ldap/config.go Normal file
View File

@ -0,0 +1,27 @@
package ldap
type Type string
const (
TypeActiveDirectory Type = "AD"
TypeOpenLDAP Type = "OpenLDAP"
)
type Config struct {
URL string `yaml:"url" envconfig:"LDAP_URL"`
StartTLS bool `yaml:"startTLS" envconfig:"LDAP_STARTTLS"`
BaseDN string `yaml:"dn" envconfig:"LDAP_BASEDN"`
BindUser string `yaml:"user" envconfig:"LDAP_USER"`
BindPass string `yaml:"pass" envconfig:"LDAP_PASSWORD"`
Type Type `yaml:"typ" envconfig:"LDAP_TYPE"` // AD for active directory, OpenLDAP for OpenLDAP
UserClass string `yaml:"userClass" envconfig:"LDAP_USER_CLASS"`
EmailAttribute string `yaml:"attrEmail" envconfig:"LDAP_ATTR_EMAIL"`
FirstNameAttribute string `yaml:"attrFirstname" envconfig:"LDAP_ATTR_FIRSTNAME"`
LastNameAttribute string `yaml:"attrLastname" envconfig:"LDAP_ATTR_LASTNAME"`
PhoneAttribute string `yaml:"attrPhone" envconfig:"LDAP_ATTR_PHONE"`
GroupMemberAttribute string `yaml:"attrGroups" envconfig:"LDAP_ATTR_GROUPS"`
DisabledAttribute string `yaml:"attrDisabled" envconfig:"LDAP_ATTR_DISABLED"`
AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"`
}

View File

@ -1,9 +1,112 @@
package ldap package ldap
type Config struct { import (
URL string `yaml:"url" envconfig:"LDAP_URL"` "crypto/tls"
StartTLS bool `yaml:"startTLS" envconfig:"LDAP_STARTTLS"` "fmt"
BaseDN string `yaml:"dn" envconfig:"LDAP_BASEDN"` "strconv"
BindUser string `yaml:"user" envconfig:"LDAP_USER"`
BindPass string `yaml:"pass" envconfig:"LDAP_PASSWORD"` "github.com/go-ldap/ldap/v3"
"github.com/pkg/errors"
)
type RawLdapData struct {
DN string
Attributes map[string]string
RawAttributes map[string][][]byte
}
func Open(cfg *Config) (*ldap.Conn, error) {
conn, err := ldap.DialURL(cfg.URL)
if err != nil {
return nil, err
}
if cfg.StartTLS {
// Reconnect with TLS
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
return nil, err
}
}
err = conn.Bind(cfg.BindUser, cfg.BindPass)
if err != nil {
return nil, err
}
return conn, nil
}
func Close(conn *ldap.Conn) {
if conn != nil {
conn.Close()
}
}
func FindAllUsers(cfg *Config) ([]RawLdapData, error) {
client, err := Open(cfg)
if err != nil {
return nil, errors.WithMessage(err, "failed to open ldap connection")
}
defer Close(client)
// Search all users
attrs := []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute,
cfg.PhoneAttribute, cfg.GroupMemberAttribute}
if cfg.DisabledAttribute != "" {
attrs = append(attrs, cfg.DisabledAttribute)
}
searchRequest := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(objectClass=%s)", cfg.UserClass), attrs, nil,
)
sr, err := client.Search(searchRequest)
if err != nil {
return nil, errors.Wrapf(err, "failed to search in ldap")
}
tmpData := make([]RawLdapData, 0, len(sr.Entries))
for _, entry := range sr.Entries {
tmp := RawLdapData{
DN: entry.DN,
Attributes: make(map[string]string, len(attrs)),
RawAttributes: make(map[string][][]byte, len(attrs)),
}
for _, field := range attrs {
tmp.Attributes[field] = entry.GetAttributeValue(field)
tmp.RawAttributes[field] = entry.GetRawAttributeValues(field)
}
tmpData = append(tmpData, tmp)
}
return tmpData, nil
}
func IsActiveDirectoryUserDisabled(userAccountControl string) bool {
if userAccountControl == "" {
return false
}
uacInt, err := strconv.Atoi(userAccountControl)
if err != nil {
return true
}
if int32(uacInt)&0x2 != 0 {
return true // bit 2 set means account is disabled
}
return false
}
func IsOpenLdapUserDisabled(pwdAccountLockedTime string) bool {
if pwdAccountLockedTime != "" {
return true
}
return false
} }

View File

@ -1,338 +0,0 @@
package ldap
import (
"crypto/md5"
"crypto/tls"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/sirupsen/logrus"
)
var Fields = []string{"givenName", "sn", "mail", "department", "memberOf", "sAMAccountName", "telephoneNumber",
"mobile", "displayName", "cn", "title", "company", "manager", "streetAddress", "employeeID", "memberOf", "l",
"st", "postalCode", "co", "facsimileTelephoneNumber", "pager", "thumbnailPhoto", "otherMobile",
"extensionAttribute2", "distinguishedName", "userAccountControl"}
// --------------------------------------------------------------------------------------------------------------------
// Cache Data Store
// --------------------------------------------------------------------------------------------------------------------
type UserCacheHolder interface {
Clear()
SetAllUsers(users []RawLdapData)
GetUser(dn string) *RawLdapData
GetUsers() []*RawLdapData
}
type RawLdapData struct {
DN string
Attributes map[string]string
RawAttributes map[string][][]byte
}
// --------------------------------------------------------------------------------------------------------------------
// Sample Cache Data store
// --------------------------------------------------------------------------------------------------------------------
type UserCacheHolderEntry struct {
RawLdapData
Username string
Mail string
Firstname string
Lastname string
Groups []string
}
func (e *UserCacheHolderEntry) CalcFieldsFromAttributes() {
e.Username = strings.ToLower(e.Attributes["sAMAccountName"])
e.Mail = e.Attributes["mail"]
e.Firstname = e.Attributes["givenName"]
e.Lastname = e.Attributes["sn"]
e.Groups = make([]string, len(e.RawAttributes["memberOf"]))
for i, group := range e.RawAttributes["memberOf"] {
e.Groups[i] = string(group)
}
}
func (e *UserCacheHolderEntry) GetUID() string {
return fmt.Sprintf("u%x", md5.Sum([]byte(e.Attributes["distinguishedName"])))
}
type SynchronizedUserCacheHolder struct {
users map[string]*UserCacheHolderEntry
mux sync.RWMutex
}
func (h *SynchronizedUserCacheHolder) Init() {
h.users = make(map[string]*UserCacheHolderEntry)
}
func (h *SynchronizedUserCacheHolder) Clear() {
h.mux.Lock()
defer h.mux.Unlock()
h.users = make(map[string]*UserCacheHolderEntry)
}
func (h *SynchronizedUserCacheHolder) SetAllUsers(users []RawLdapData) {
h.mux.Lock()
defer h.mux.Unlock()
h.users = make(map[string]*UserCacheHolderEntry)
for i := range users {
h.users[users[i].DN] = &UserCacheHolderEntry{RawLdapData: users[i]}
h.users[users[i].DN].CalcFieldsFromAttributes()
}
}
func (h *SynchronizedUserCacheHolder) GetUser(dn string) *RawLdapData {
h.mux.RLock()
defer h.mux.RUnlock()
return &h.users[dn].RawLdapData
}
func (h *SynchronizedUserCacheHolder) GetUserData(dn string) *UserCacheHolderEntry {
h.mux.RLock()
defer h.mux.RUnlock()
return h.users[dn]
}
func (h *SynchronizedUserCacheHolder) GetUsers() []*RawLdapData {
h.mux.RLock()
defer h.mux.RUnlock()
users := make([]*RawLdapData, 0, len(h.users))
for _, user := range h.users {
users = append(users, &user.RawLdapData)
}
return users
}
func (h *SynchronizedUserCacheHolder) GetSortedUsers(sortKey string, sortDirection string) []*UserCacheHolderEntry {
h.mux.RLock()
defer h.mux.RUnlock()
sortedUsers := make([]*UserCacheHolderEntry, 0, len(h.users))
for _, user := range h.users {
sortedUsers = append(sortedUsers, user)
}
sort.Slice(sortedUsers, func(i, j int) bool {
if sortDirection == "asc" {
return sortedUsers[i].Attributes[sortKey] < sortedUsers[j].Attributes[sortKey]
} else {
return sortedUsers[i].Attributes[sortKey] > sortedUsers[j].Attributes[sortKey]
}
})
return sortedUsers
}
func (h *SynchronizedUserCacheHolder) IsInGroup(username, gid string) bool {
userDN := h.GetUserDN(username)
if userDN == "" {
return false // user not found -> not in group
}
user := h.GetUserData(userDN)
if user == nil {
return false
}
for _, group := range user.Groups {
if group == gid {
return true
}
}
return false
}
func (h *SynchronizedUserCacheHolder) UserExists(username string) bool {
userDN := h.GetUserDN(username)
if userDN == "" {
return false // user not found
}
return true
}
func (h *SynchronizedUserCacheHolder) GetUserDN(username string) string {
userDN := ""
for dn, user := range h.users {
accName := strings.ToLower(user.Attributes["sAMAccountName"])
if accName == username {
userDN = dn
break
}
}
return userDN
}
func (h *SynchronizedUserCacheHolder) GetUserDNByMail(mail string) string {
userDN := ""
for dn, user := range h.users {
accMail := strings.ToLower(user.Attributes["mail"])
if accMail == mail {
userDN = dn
break
}
}
return userDN
}
// --------------------------------------------------------------------------------------------------------------------
// Cache Handler, LDAP interaction
// --------------------------------------------------------------------------------------------------------------------
type UserCache struct {
Cfg *Config
LastError error
UpdatedAt time.Time
userData UserCacheHolder
}
func NewUserCache(config Config, store UserCacheHolder) *UserCache {
uc := &UserCache{
Cfg: &config,
UpdatedAt: time.Now(),
userData: store,
}
logrus.Infof("Filling user cache...")
err := uc.Update(true, true)
logrus.Infof("User cache filled!")
uc.LastError = err
return uc
}
func (u UserCache) open() (*ldap.Conn, error) {
conn, err := ldap.DialURL(u.Cfg.URL)
if err != nil {
return nil, err
}
if u.Cfg.StartTLS {
// Reconnect with TLS
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
return nil, err
}
}
err = conn.Bind(u.Cfg.BindUser, u.Cfg.BindPass)
if err != nil {
return nil, err
}
return conn, nil
}
func (u UserCache) close(conn *ldap.Conn) {
if conn != nil {
conn.Close()
}
}
// Update updates the user cache in background, minimal locking will happen
func (u *UserCache) Update(filter, withDisabledUsers bool) error {
logrus.Debugf("Updating ldap cache...")
client, err := u.open()
if err != nil {
u.LastError = err
return err
}
defer u.close(client)
// Search for the given username
searchRequest := ldap.NewSearchRequest(
u.Cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
"(objectClass=organizationalPerson)",
Fields,
nil,
)
sr, err := client.Search(searchRequest)
if err != nil {
u.LastError = err
return err
}
tmpData := make([]RawLdapData, 0, len(sr.Entries))
for _, entry := range sr.Entries {
if filter {
usernameAttr := strings.ToLower(entry.GetAttributeValue("sAMAccountName"))
firstNameAttr := entry.GetAttributeValue("givenName")
lastNameAttr := entry.GetAttributeValue("sn")
mailAttr := entry.GetAttributeValue("mail")
userAccountControl := entry.GetAttributeValue("userAccountControl")
employeeID := entry.GetAttributeValue("employeeID")
dn := entry.GetAttributeValue("distinguishedName")
if usernameAttr == "" || firstNameAttr == "" || lastNameAttr == "" || mailAttr == "" || employeeID == "" {
continue // prefilter...
}
if !withDisabledUsers && userAccountControl != "" && IsLdapUserDisabled(userAccountControl) {
continue
}
if entry.DN != dn {
logrus.Errorf("LDAP inconsistent: '%s' != '%s'", entry.DN, dn)
continue
}
}
tmp := RawLdapData{
DN: entry.DN,
Attributes: make(map[string]string, len(Fields)),
RawAttributes: make(map[string][][]byte, len(Fields)),
}
for _, field := range Fields {
tmp.Attributes[field] = entry.GetAttributeValue(field)
tmp.RawAttributes[field] = entry.GetRawAttributeValues(field)
}
tmpData = append(tmpData, tmp)
}
// Copy to userdata
u.userData.SetAllUsers(tmpData)
u.UpdatedAt = time.Now()
u.LastError = nil
logrus.Debug("Ldap cache updated...")
return nil
}
func IsLdapUserDisabled(userAccountControl string) bool {
uacInt, err := strconv.Atoi(userAccountControl)
if err != nil {
return true
}
if int32(uacInt)&0x2 != 0 {
return true // bit 2 set means account is disabled
}
return false
}

83
internal/server/auth.go Normal file
View File

@ -0,0 +1,83 @@
package server
import (
"sort"
"github.com/h44z/wg-portal/internal/authentication"
"github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/users"
"github.com/sirupsen/logrus"
)
// Auth auth struct
type AuthManager struct {
Server *Server
Group *gin.RouterGroup // basic group for all providers (/auth)
providers []authentication.AuthProvider
UserManager *users.Manager
}
// RegisterProvider register auth provider
func (auth *AuthManager) RegisterProvider(provider authentication.AuthProvider) {
name := provider.GetName()
if auth.GetProvider(name) != nil {
logrus.Warnf("auth provider %v already registered", name)
}
provider.SetupRoutes(auth.Group)
auth.providers = append(auth.providers, provider)
}
// RegisterProviderWithoutError register auth provider if err is nil
func (auth *AuthManager) RegisterProviderWithoutError(provider authentication.AuthProvider, err error) {
if err != nil {
logrus.Errorf("skipping provider registration: %v", err)
return
}
auth.RegisterProvider(provider)
}
// GetProvider get provider with name
func (auth *AuthManager) GetProvider(name string) authentication.AuthProvider {
for _, provider := range auth.providers {
if provider.GetName() == name {
return provider
}
}
return nil
}
// GetProviders return registered providers
func (auth *AuthManager) GetProviders() (providers []authentication.AuthProvider) {
for _, provider := range auth.providers {
providers = append(providers, provider)
}
return
}
// GetProviders return registered providers
func (auth *AuthManager) GetProvidersForType(typ authentication.AuthProviderType) (providers []authentication.AuthProvider) {
for _, provider := range auth.providers {
if provider.GetType() == typ {
providers = append(providers, provider)
}
}
// order by priority
sort.SliceStable(providers, func(i, j int) bool {
return providers[i].GetPriority() < providers[j].GetPriority()
})
return
}
func NewAuthManager(server *Server) *AuthManager {
m := &AuthManager{
Server: server,
}
m.Group = m.Server.server.Group("/auth")
return m
}

View File

@ -3,10 +3,11 @@ package server
import ( import (
"context" "context"
"encoding/gob" "encoding/gob"
"errors"
"html/template" "html/template"
"io/fs"
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -15,15 +16,18 @@ import (
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore" "github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
wg_portal "github.com/h44z/wg-portal"
ldapprovider "github.com/h44z/wg-portal/internal/authentication/providers/ldap"
passwordprovider "github.com/h44z/wg-portal/internal/authentication/providers/password"
"github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/ldap" "github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard" "github.com/h44z/wg-portal/internal/wireguard"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
ginlogrus "github.com/toorop/gin-logrus" ginlogrus "github.com/toorop/gin-logrus"
) )
const SessionIdentifier = "wgPortalSession" const SessionIdentifier = "wgPortalSession"
const CacheRefreshDuration = 5 * time.Minute
func init() { func init() {
gob.Register(SessionData{}) gob.Register(SessionData{})
@ -31,19 +35,20 @@ func init() {
gob.Register(Peer{}) gob.Register(Peer{})
gob.Register(Device{}) gob.Register(Device{})
gob.Register(LdapCreateForm{}) gob.Register(LdapCreateForm{})
gob.Register(users.User{})
} }
type SessionData struct { type SessionData struct {
LoggedIn bool LoggedIn bool
IsAdmin bool IsAdmin bool
UID string
UserName string
Firstname string Firstname string
Lastname string Lastname string
Email string Email string
SortedBy string
SortDirection string SortedBy map[string]string
Search string SortDirection map[string]string
Search map[string]string
AlertData string AlertData string
AlertType string AlertType string
FormData interface{} FormData interface{}
@ -60,28 +65,23 @@ type StaticData struct {
WebsiteLogo string WebsiteLogo string
CompanyName string CompanyName string
Year int Year int
LdapDisabled bool
} }
type Server struct { type Server struct {
// Core components
ctx context.Context ctx context.Context
config *common.Config config *common.Config
server *gin.Engine server *gin.Engine
users *UserManager
mailTpl *template.Template mailTpl *template.Template
auth *AuthManager
// WireGuard stuff users *users.Manager
wg *wireguard.Manager wg *wireguard.Manager
peers *PeerManager
// LDAP stuff
ldapDisabled bool
ldapAuth ldap.Authentication
ldapUsers *ldap.SynchronizedUserCacheHolder
ldapCacheUpdater *ldap.UserCache
} }
func (s *Server) Setup(ctx context.Context) error { func (s *Server) Setup(ctx context.Context) error {
var err error
dir := s.getExecutableDirectory() dir := s.getExecutableDirectory()
rDir, _ := filepath.Abs(filepath.Dir(os.Args[0])) rDir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
logrus.Infof("Real working directory: %s", rDir) logrus.Infof("Real working directory: %s", rDir)
@ -91,44 +91,10 @@ func (s *Server) Setup(ctx context.Context) error {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
s.config = common.NewConfig() s.config = common.NewConfig()
s.ctx = ctx
// Setup LDAP stuff
s.ldapAuth = ldap.NewAuthentication(s.config.LDAP)
s.ldapUsers = &ldap.SynchronizedUserCacheHolder{}
s.ldapUsers.Init()
s.ldapCacheUpdater = ldap.NewUserCache(s.config.LDAP, s.ldapUsers)
if s.ldapCacheUpdater.LastError != nil {
logrus.Warnf("LDAP error: %v", s.ldapCacheUpdater.LastError)
logrus.Warnf("LDAP features disabled!")
s.ldapDisabled = true
}
// Setup WireGuard stuff
s.wg = &wireguard.Manager{Cfg: &s.config.WG}
if err := s.wg.Init(); err != nil {
return err
}
// Setup user manager
if s.users = NewUserManager(filepath.Join(dir, s.config.Core.DatabasePath), s.wg, s.ldapUsers); s.users == nil {
return errors.New("unable to setup user manager")
}
if err := s.users.InitFromCurrentInterface(); err != nil {
return errors.New("unable to initialize user manager")
}
if err := s.RestoreWireGuardInterface(); err != nil {
return errors.New("unable to restore WireGuard state")
}
// Setup mail template
var err error
s.mailTpl, err = template.New("email.html").ParseFiles(filepath.Join(dir, "/assets/tpl/email.html"))
if err != nil {
return errors.New("unable to pare mail template")
}
// Setup http server // Setup http server
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.DebugMode)
gin.DefaultWriter = ioutil.Discard gin.DefaultWriter = ioutil.Discard
s.server = gin.New() s.server = gin.New()
s.server.Use(ginlogrus.Logger(logrus.StandardLogger()), gin.Recovery()) s.server.Use(ginlogrus.Logger(logrus.StandardLogger()), gin.Recovery())
@ -138,54 +104,98 @@ func (s *Server) Setup(ctx context.Context) error {
}) })
// Setup templates // Setup templates
logrus.Infof("Loading templates from: %s", filepath.Join(dir, "/assets/tpl/*.html")) templates := template.Must(template.New("").Funcs(s.server.FuncMap).ParseFS(wg_portal.Templates, "assets/tpl/*.html"))
s.server.LoadHTMLGlob(filepath.Join(dir, "/assets/tpl/*.html")) s.server.SetHTMLTemplate(templates)
s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte("secret")))) // TODO: change key? s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte("secret")))) // TODO: change key?
// Serve static files // Serve static files
s.server.Static("/css", filepath.Join(dir, "/assets/css")) s.server.StaticFS("/css", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/css"))))
s.server.Static("/js", filepath.Join(dir, "/assets/js")) s.server.StaticFS("/js", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/js"))))
s.server.Static("/img", filepath.Join(dir, "/assets/img")) s.server.StaticFS("/img", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/img"))))
s.server.Static("/fonts", filepath.Join(dir, "/assets/fonts")) s.server.StaticFS("/fonts", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/fonts"))))
// Setup all routes // Setup all routes
SetupRoutes(s) SetupRoutes(s)
// Setup user database (also needed for database authentication)
s.users, err = users.NewManager(&s.config.Database)
if err != nil {
return errors.WithMessage(err, "user-manager initialization failed")
}
// Setup auth manager
s.auth = NewAuthManager(s)
pwProvider, err := passwordprovider.New(&s.config.Database)
if err != nil {
return errors.WithMessage(err, "password provider initialization failed")
}
if err = pwProvider.InitializeAdmin(s.config.Core.AdminUser, s.config.Core.AdminPassword); err != nil {
return errors.WithMessage(err, "admin initialization failed")
}
s.auth.RegisterProvider(pwProvider)
if s.config.Core.LdapEnabled {
ldapProvider, err := ldapprovider.New(&s.config.LDAP)
if err != nil {
s.config.Core.LdapEnabled = false
logrus.Warnf("failed to setup LDAP connection, LDAP features disabled")
}
s.auth.RegisterProviderWithoutError(ldapProvider, err)
}
// Setup WireGuard stuff
s.wg = &wireguard.Manager{Cfg: &s.config.WG}
if err = s.wg.Init(); err != nil {
return errors.WithMessage(err, "unable to initialize WireGuard manager")
}
// Setup peer manager
if s.peers, err = NewPeerManager(s.config, s.wg, s.users); err != nil {
return errors.WithMessage(err, "unable to setup peer manager")
}
if err = s.peers.InitFromCurrentInterface(); err != nil {
return errors.WithMessage(err, "unable to initialize peer manager")
}
if err = s.RestoreWireGuardInterface(); err != nil {
return errors.WithMessage(err, "unable to restore WireGuard state")
}
// Setup mail template
s.mailTpl, err = template.New("email.html").ParseFS(wg_portal.Templates, "assets/tpl/email.html")
if err != nil {
return errors.Wrap(err, "unable to pare mail template")
}
logrus.Infof("Setup of service completed!") logrus.Infof("Setup of service completed!")
return nil return nil
} }
func (s *Server) Run() { func (s *Server) Run() {
// Start ldap group watcher // Start ldap sync
if !s.ldapDisabled { if s.config.Core.LdapEnabled {
go func(s *Server) { go s.SyncLdapWithUserDatabase()
for {
time.Sleep(CacheRefreshDuration)
if err := s.ldapCacheUpdater.Update(true, true); err != nil {
logrus.Warnf("Failed to update ldap group cache: %v", err)
}
logrus.Debugf("Refreshed LDAP permissions!")
}
}(s)
}
if !s.ldapDisabled && s.config.Core.SyncLdapStatus {
go func(s *Server) {
for {
time.Sleep(CacheRefreshDuration)
if err := s.SyncLdapAttributesWithWireGuard(); err != nil {
logrus.Warnf("Failed to synchronize ldap attributes: %v", err)
}
logrus.Debugf("Synced LDAP attributes!")
}
}(s)
} }
// Run web service // Run web service
err := s.server.Run(s.config.Core.ListeningAddress) srv := &http.Server{
if err != nil { Addr: s.config.Core.ListeningAddress,
logrus.Errorf("Failed to listen and serve on %s: %v", s.config.Core.ListeningAddress, err) Handler: s.server,
} }
go func() {
if err := srv.ListenAndServe(); err != nil {
logrus.Debugf("web service on %s exited: %v", s.config.Core.ListeningAddress, err)
}
}()
<-s.ctx.Done()
logrus.Debug("web service shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
} }
func (s *Server) getExecutableDirectory() string { func (s *Server) getExecutableDirectory() string {
@ -201,7 +211,16 @@ func (s *Server) getExecutableDirectory() string {
return dir return dir
} }
func (s *Server) getSessionData(c *gin.Context) SessionData { func (s *Server) getStaticData() StaticData {
return StaticData{
WebsiteTitle: s.config.Core.Title,
WebsiteLogo: "/img/header-logo.png",
CompanyName: s.config.Core.CompanyName,
Year: time.Now().Year(),
}
}
func GetSessionData(c *gin.Context) SessionData {
session := sessions.Default(c) session := sessions.Default(c)
rawSessionData := session.Get(SessionIdentifier) rawSessionData := session.Get(SessionIdentifier)
@ -210,8 +229,9 @@ func (s *Server) getSessionData(c *gin.Context) SessionData {
sessionData = rawSessionData.(SessionData) sessionData = rawSessionData.(SessionData)
} else { } else {
sessionData = SessionData{ sessionData = SessionData{
SortedBy: "mail", Search: map[string]string{"peers": "", "userpeers": "", "users": ""},
SortDirection: "asc", SortedBy: map[string]string{"peers": "mail", "userpeers": "mail", "users": "email"},
SortDirection: map[string]string{"peers": "asc", "userpeers": "asc", "users": "asc"},
Email: "", Email: "",
Firstname: "", Firstname: "",
Lastname: "", Lastname: "",
@ -227,7 +247,7 @@ func (s *Server) getSessionData(c *gin.Context) SessionData {
return sessionData return sessionData
} }
func (s *Server) getFlashes(c *gin.Context) []FlashData { func GetFlashes(c *gin.Context) []FlashData {
session := sessions.Default(c) session := sessions.Default(c)
flashes := session.Flashes() flashes := session.Flashes()
if err := session.Save(); err != nil { if err := session.Save(); err != nil {
@ -242,7 +262,7 @@ func (s *Server) getFlashes(c *gin.Context) []FlashData {
return flashData return flashData
} }
func (s *Server) updateSessionData(c *gin.Context, data SessionData) error { func UpdateSessionData(c *gin.Context, data SessionData) error {
session := sessions.Default(c) session := sessions.Default(c)
session.Set(SessionIdentifier, data) session.Set(SessionIdentifier, data)
if err := session.Save(); err != nil { if err := session.Save(); err != nil {
@ -252,7 +272,7 @@ func (s *Server) updateSessionData(c *gin.Context, data SessionData) error {
return nil return nil
} }
func (s *Server) destroySessionData(c *gin.Context) error { func DestroySessionData(c *gin.Context) error {
session := sessions.Default(c) session := sessions.Default(c)
session.Delete(SessionIdentifier) session.Delete(SessionIdentifier)
if err := session.Save(); err != nil { if err := session.Save(); err != nil {
@ -262,17 +282,7 @@ func (s *Server) destroySessionData(c *gin.Context) error {
return nil return nil
} }
func (s *Server) getStaticData() StaticData { func SetFlashMessage(c *gin.Context, message, typ string) {
return StaticData{
WebsiteTitle: s.config.Core.Title,
WebsiteLogo: "/img/header-logo.png",
CompanyName: s.config.Core.CompanyName,
LdapDisabled: s.ldapDisabled,
Year: time.Now().Year(),
}
}
func (s *Server) setFlashMessage(c *gin.Context, message, typ string) {
session := sessions.Default(c) session := sessions.Default(c)
session.AddFlash(FlashData{ session.AddFlash(FlashData{
Message: message, Message: message,
@ -283,13 +293,20 @@ func (s *Server) setFlashMessage(c *gin.Context, message, typ string) {
} }
} }
func (s SessionData) GetSortIcon(field string) string { func (s SessionData) GetSortIcon(table, field string) string {
if s.SortedBy != field { if s.SortedBy[table] != field {
return "fa-sort" return "fa-sort"
} }
if s.SortDirection == "asc" { if s.SortDirection[table] == "asc" {
return "fa-sort-alpha-down" return "fa-sort-alpha-down"
} else { } else {
return "fa-sort-alpha-up" return "fa-sort-alpha-up"
} }
} }
func fsMust(f fs.FS, err error) fs.FS {
if err != nil {
panic(err)
}
return f
}

View File

@ -5,11 +5,14 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/authentication"
"github.com/h44z/wg-portal/internal/users"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm"
) )
func (s *Server) GetLogin(c *gin.Context) { func (s *Server) GetLogin(c *gin.Context) {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
if currentSession.LoggedIn { if currentSession.LoggedIn {
c.Redirect(http.StatusSeeOther, "/") // already logged in c.Redirect(http.StatusSeeOther, "/") // already logged in
} }
@ -33,7 +36,7 @@ func (s *Server) GetLogin(c *gin.Context) {
} }
func (s *Server) PostLogin(c *gin.Context) { func (s *Server) PostLogin(c *gin.Context) {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
if currentSession.LoggedIn { if currentSession.LoggedIn {
// already logged in // already logged in
c.Redirect(http.StatusSeeOther, "/") c.Redirect(http.StatusSeeOther, "/")
@ -49,59 +52,84 @@ func (s *Server) PostLogin(c *gin.Context) {
return return
} }
adminAuthenticated := false // Check user database for an matching entry
if s.config.Core.AdminUser != "" && username == s.config.Core.AdminUser && password == s.config.Core.AdminPassword { var loginProvider authentication.AuthProvider
adminAuthenticated = true email := ""
user := s.users.GetUser(username) // retrieve active candidate user from db
if user != nil { // existing user
loginProvider = s.auth.GetProvider(string(user.Source))
if loginProvider == nil {
s.GetHandleError(c, http.StatusInternalServerError, "login error", "login provider unavailable")
return
}
authEmail, err := loginProvider.Login(&authentication.AuthContext{
Username: username,
Password: password,
})
if err == nil {
email = authEmail
}
} else { // possible new user
// Check all available auth backends
for _, provider := range s.auth.GetProvidersForType(authentication.AuthProviderTypePassword) {
// try to log in to the given provider
authEmail, err := provider.Login(&authentication.AuthContext{
Username: username,
Password: password,
})
if err != nil {
continue
} }
// Check if user is in cache, avoid unnecessary ldap requests email = authEmail
if !adminAuthenticated && !s.ldapUsers.UserExists(username) { loginProvider = provider
c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail")
// create new user in the database (or reactivate him)
if user, err = s.users.GetOrCreateUserUnscoped(email); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "login error", "failed to create new user")
return
}
userData, err := loginProvider.GetUserModel(&authentication.AuthContext{
Username: email,
})
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "login error", err.Error())
return
}
user.Firstname = userData.Firstname
user.Lastname = userData.Lastname
user.Email = userData.Email
user.Phone = userData.Phone
user.IsAdmin = userData.IsAdmin
user.Source = users.UserSource(loginProvider.GetName())
user.DeletedAt = gorm.DeletedAt{} // reset deleted flag
if err = s.users.UpdateUser(user); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "login error", "failed to update user data")
return
}
break
}
} }
// Check if username and password match // Check if user is authenticated
if !adminAuthenticated && !s.ldapAuth.CheckLogin(username, password) { if email == "" || loginProvider == nil {
c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail") c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail")
return return
} }
var sessionData SessionData // Set authenticated session
if adminAuthenticated { sessionData := GetSessionData(c)
sessionData = SessionData{ sessionData.LoggedIn = true
LoggedIn: true, sessionData.IsAdmin = user.IsAdmin
IsAdmin: true, sessionData.Email = user.Email
Email: "autodetected@example.com", sessionData.Firstname = user.Firstname
UID: "adminuid", sessionData.Lastname = user.Lastname
UserName: username,
Firstname: "System",
Lastname: "Administrator",
SortedBy: "mail",
SortDirection: "asc",
Search: "",
}
} else {
dn := s.ldapUsers.GetUserDN(username)
userData := s.ldapUsers.GetUserData(dn)
sessionData = SessionData{
LoggedIn: true,
IsAdmin: s.ldapUsers.IsInGroup(username, s.config.AdminLdapGroup),
UID: userData.GetUID(),
UserName: username,
Email: userData.Mail,
Firstname: userData.Firstname,
Lastname: userData.Lastname,
SortedBy: "mail",
SortDirection: "asc",
Search: "",
}
}
// Check if user already has a peer setup, if not create one // Check if user already has a peer setup, if not create one
if s.config.Core.CreateInterfaceOnLogin && !adminAuthenticated { if s.config.Core.CreateDefaultPeer {
users := s.users.GetUsersByMail(sessionData.Email) peers := s.peers.GetPeersByMail(sessionData.Email)
if len(peers) == 0 { // Create vpn peer
if len(users) == 0 { // Create vpn peer err := s.CreatePeer(Peer{
err := s.CreateUser(Peer{
Identifier: sessionData.Firstname + " " + sessionData.Lastname + " (Default)", Identifier: sessionData.Firstname + " " + sessionData.Lastname + " (Default)",
Email: sessionData.Email, Email: sessionData.Email,
CreatedBy: sessionData.Email, CreatedBy: sessionData.Email,
@ -111,7 +139,7 @@ func (s *Server) PostLogin(c *gin.Context) {
} }
} }
if err := s.updateSessionData(c, sessionData); err != nil { if err := UpdateSessionData(c, sessionData); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "login error", "failed to save session") s.GetHandleError(c, http.StatusInternalServerError, "login error", "failed to save session")
return return
} }
@ -119,14 +147,14 @@ func (s *Server) PostLogin(c *gin.Context) {
} }
func (s *Server) GetLogout(c *gin.Context) { func (s *Server) GetLogout(c *gin.Context) {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
if !currentSession.LoggedIn { // Not logged in if !currentSession.LoggedIn { // Not logged in
c.Redirect(http.StatusSeeOther, "/") c.Redirect(http.StatusSeeOther, "/")
return return
} }
if err := s.destroySessionData(c); err != nil { if err := DestroySessionData(c); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "logout error", "failed to destroy session") s.GetHandleError(c, http.StatusInternalServerError, "logout error", "failed to destroy session")
return return
} }

View File

@ -15,7 +15,7 @@ func (s *Server) GetHandleError(c *gin.Context, code int, message, details strin
"Details": details, "Details": details,
}, },
"Route": c.Request.URL.Path, "Route": c.Request.URL.Path,
"Session": s.getSessionData(c), "Session": GetSessionData(c),
"Static": s.getStaticData(), "Static": s.getStaticData(),
}) })
} }
@ -29,51 +29,51 @@ func (s *Server) GetIndex(c *gin.Context) {
Device Device Device Device
}{ }{
Route: c.Request.URL.Path, Route: c.Request.URL.Path,
Alerts: s.getFlashes(c), Alerts: GetFlashes(c),
Session: s.getSessionData(c), Session: GetSessionData(c),
Static: s.getStaticData(), Static: s.getStaticData(),
Device: s.users.GetDevice(), Device: s.peers.GetDevice(),
}) })
} }
func (s *Server) GetAdminIndex(c *gin.Context) { func (s *Server) GetAdminIndex(c *gin.Context) {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
sort := c.Query("sort") sort := c.Query("sort")
if sort != "" { if sort != "" {
if currentSession.SortedBy != sort { if currentSession.SortedBy["peers"] != sort {
currentSession.SortedBy = sort currentSession.SortedBy["peers"] = sort
currentSession.SortDirection = "asc" currentSession.SortDirection["peers"] = "asc"
} else { } else {
if currentSession.SortDirection == "asc" { if currentSession.SortDirection["peers"] == "asc" {
currentSession.SortDirection = "desc" currentSession.SortDirection["peers"] = "desc"
} else { } else {
currentSession.SortDirection = "asc" currentSession.SortDirection["peers"] = "asc"
} }
} }
if err := s.updateSessionData(c, currentSession); err != nil { if err := UpdateSessionData(c, currentSession); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "sort error", "failed to save session") s.GetHandleError(c, http.StatusInternalServerError, "sort error", "failed to save session")
return return
} }
c.Redirect(http.StatusSeeOther, "/admin") c.Redirect(http.StatusSeeOther, "/admin/")
return return
} }
search, searching := c.GetQuery("search") search, searching := c.GetQuery("search")
if searching { if searching {
currentSession.Search = search currentSession.Search["peers"] = search
if err := s.updateSessionData(c, currentSession); err != nil { if err := UpdateSessionData(c, currentSession); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "search error", "failed to save session") s.GetHandleError(c, http.StatusInternalServerError, "search error", "failed to save session")
return return
} }
c.Redirect(http.StatusSeeOther, "/admin") c.Redirect(http.StatusSeeOther, "/admin/")
return return
} }
device := s.users.GetDevice() device := s.peers.GetDevice()
users := s.users.GetFilteredAndSortedUsers(currentSession.SortedBy, currentSession.SortDirection, currentSession.Search) users := s.peers.GetFilteredAndSortedPeers(currentSession.SortedBy["peers"], currentSession.SortDirection["peers"], currentSession.Search["peers"])
c.HTML(http.StatusOK, "admin_index.html", struct { c.HTML(http.StatusOK, "admin_index.html", struct {
Route string Route string
@ -83,36 +83,34 @@ func (s *Server) GetAdminIndex(c *gin.Context) {
Peers []Peer Peers []Peer
TotalPeers int TotalPeers int
Device Device Device Device
LdapDisabled bool
}{ }{
Route: c.Request.URL.Path, Route: c.Request.URL.Path,
Alerts: s.getFlashes(c), Alerts: GetFlashes(c),
Session: currentSession, Session: currentSession,
Static: s.getStaticData(), Static: s.getStaticData(),
Peers: users, Peers: users,
TotalPeers: len(s.users.GetAllUsers()), TotalPeers: len(s.peers.GetAllPeers()),
Device: device, Device: device,
LdapDisabled: s.ldapDisabled,
}) })
} }
func (s *Server) GetUserIndex(c *gin.Context) { func (s *Server) GetUserIndex(c *gin.Context) {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
sort := c.Query("sort") sort := c.Query("sort")
if sort != "" { if sort != "" {
if currentSession.SortedBy != sort { if currentSession.SortedBy["userpeers"] != sort {
currentSession.SortedBy = sort currentSession.SortedBy["userpeers"] = sort
currentSession.SortDirection = "asc" currentSession.SortDirection["userpeers"] = "asc"
} else { } else {
if currentSession.SortDirection == "asc" { if currentSession.SortDirection["userpeers"] == "asc" {
currentSession.SortDirection = "desc" currentSession.SortDirection["userpeers"] = "desc"
} else { } else {
currentSession.SortDirection = "asc" currentSession.SortDirection["userpeers"] = "asc"
} }
} }
if err := s.updateSessionData(c, currentSession); err != nil { if err := UpdateSessionData(c, currentSession); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "sort error", "failed to save session") s.GetHandleError(c, http.StatusInternalServerError, "sort error", "failed to save session")
return return
} }
@ -120,8 +118,8 @@ func (s *Server) GetUserIndex(c *gin.Context) {
return return
} }
device := s.users.GetDevice() device := s.peers.GetDevice()
users := s.users.GetSortedUsersForEmail(currentSession.SortedBy, currentSession.SortDirection, currentSession.Email) users := s.peers.GetSortedPeersForEmail(currentSession.SortedBy["userpeers"], currentSession.SortDirection["userpeers"], currentSession.Email)
c.HTML(http.StatusOK, "user_index.html", struct { c.HTML(http.StatusOK, "user_index.html", struct {
Route string Route string
@ -133,7 +131,7 @@ func (s *Server) GetUserIndex(c *gin.Context) {
Device Device Device Device
}{ }{
Route: c.Request.URL.Path, Route: c.Request.URL.Path,
Alerts: s.getFlashes(c), Alerts: GetFlashes(c),
Session: currentSession, Session: currentSession,
Static: s.getStaticData(), Static: s.getStaticData(),
Peers: users, Peers: users,
@ -143,29 +141,29 @@ func (s *Server) GetUserIndex(c *gin.Context) {
} }
func (s *Server) updateFormInSession(c *gin.Context, formData interface{}) error { func (s *Server) updateFormInSession(c *gin.Context, formData interface{}) error {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
currentSession.FormData = formData currentSession.FormData = formData
if err := s.updateSessionData(c, currentSession); err != nil { if err := UpdateSessionData(c, currentSession); err != nil {
return err return err
} }
return nil return nil
} }
func (s *Server) setNewUserFormInSession(c *gin.Context) (SessionData, error) { func (s *Server) setNewPeerFormInSession(c *gin.Context) (SessionData, error) {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
// If session does not contain a user form ignore update // If session does not contain a peer form ignore update
// If url contains a formerr parameter reset the form // If url contains a formerr parameter reset the form
if currentSession.FormData == nil || c.Query("formerr") == "" { if currentSession.FormData == nil || c.Query("formerr") == "" {
user, err := s.PrepareNewUser() user, err := s.PrepareNewPeer()
if err != nil { if err != nil {
return currentSession, err return currentSession, err
} }
currentSession.FormData = user currentSession.FormData = user
} }
if err := s.updateSessionData(c, currentSession); err != nil { if err := UpdateSessionData(c, currentSession); err != nil {
return currentSession, err return currentSession, err
} }
@ -173,14 +171,14 @@ func (s *Server) setNewUserFormInSession(c *gin.Context) (SessionData, error) {
} }
func (s *Server) setFormInSession(c *gin.Context, formData interface{}) (SessionData, error) { func (s *Server) setFormInSession(c *gin.Context, formData interface{}) (SessionData, error) {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
// If session does not contain a form ignore update // If session does not contain a form ignore update
// If url contains a formerr parameter reset the form // If url contains a formerr parameter reset the form
if currentSession.FormData == nil || c.Query("formerr") == "" { if currentSession.FormData == nil || c.Query("formerr") == "" {
currentSession.FormData = formData currentSession.FormData = formData
} }
if err := s.updateSessionData(c, currentSession); err != nil { if err := UpdateSessionData(c, currentSession); err != nil {
return currentSession, err return currentSession, err
} }

View File

@ -9,8 +9,8 @@ import (
) )
func (s *Server) GetAdminEditInterface(c *gin.Context) { func (s *Server) GetAdminEditInterface(c *gin.Context) {
device := s.users.GetDevice() device := s.peers.GetDevice()
users := s.users.GetAllUsers() users := s.peers.GetAllPeers()
currentSession, err := s.setFormInSession(c, device) currentSession, err := s.setFormInSession(c, device)
if err != nil { if err != nil {
@ -28,7 +28,7 @@ func (s *Server) GetAdminEditInterface(c *gin.Context) {
EditableKeys bool EditableKeys bool
}{ }{
Route: c.Request.URL.Path, Route: c.Request.URL.Path,
Alerts: s.getFlashes(c), Alerts: GetFlashes(c),
Session: currentSession, Session: currentSession,
Static: s.getStaticData(), Static: s.getStaticData(),
Peers: users, Peers: users,
@ -38,14 +38,14 @@ func (s *Server) GetAdminEditInterface(c *gin.Context) {
} }
func (s *Server) PostAdminEditInterface(c *gin.Context) { func (s *Server) PostAdminEditInterface(c *gin.Context) {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
var formDevice Device var formDevice Device
if currentSession.FormData != nil { if currentSession.FormData != nil {
formDevice = currentSession.FormData.(Device) formDevice = currentSession.FormData.(Device)
} }
if err := c.ShouldBind(&formDevice); err != nil { if err := c.ShouldBind(&formDevice); err != nil {
_ = s.updateFormInSession(c, formDevice) _ = s.updateFormInSession(c, formDevice)
s.setFlashMessage(c, err.Error(), "danger") SetFlashMessage(c, err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=bind") c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=bind")
return return
} }
@ -61,16 +61,16 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetConfig()) err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetConfig())
if err != nil { if err != nil {
_ = s.updateFormInSession(c, formDevice) _ = s.updateFormInSession(c, formDevice)
s.setFlashMessage(c, "Failed to update device in WireGuard: "+err.Error(), "danger") SetFlashMessage(c, "Failed to update device in WireGuard: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=wg") c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=wg")
return return
} }
// Update in database // Update in database
err = s.users.UpdateDevice(formDevice) err = s.peers.UpdateDevice(formDevice)
if err != nil { if err != nil {
_ = s.updateFormInSession(c, formDevice) _ = s.updateFormInSession(c, formDevice)
s.setFlashMessage(c, "Failed to update device in database: "+err.Error(), "danger") SetFlashMessage(c, "Failed to update device in database: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update") c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
return return
} }
@ -79,7 +79,7 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
err = s.WriteWireGuardConfigFile() err = s.WriteWireGuardConfigFile()
if err != nil { if err != nil {
_ = s.updateFormInSession(c, formDevice) _ = s.updateFormInSession(c, formDevice)
s.setFlashMessage(c, "Failed to update WireGuard config-file: "+err.Error(), "danger") SetFlashMessage(c, "Failed to update WireGuard config-file: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update") c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
return return
} }
@ -88,26 +88,26 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
if s.config.WG.ManageIPAddresses { if s.config.WG.ManageIPAddresses {
if err := s.wg.SetIPAddress(formDevice.IPs); err != nil { if err := s.wg.SetIPAddress(formDevice.IPs); err != nil {
_ = s.updateFormInSession(c, formDevice) _ = s.updateFormInSession(c, formDevice)
s.setFlashMessage(c, "Failed to update ip address: "+err.Error(), "danger") SetFlashMessage(c, "Failed to update ip address: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update") c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
} }
if err := s.wg.SetMTU(formDevice.Mtu); err != nil { if err := s.wg.SetMTU(formDevice.Mtu); err != nil {
_ = s.updateFormInSession(c, formDevice) _ = s.updateFormInSession(c, formDevice)
s.setFlashMessage(c, "Failed to update MTU: "+err.Error(), "danger") SetFlashMessage(c, "Failed to update MTU: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update") c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
} }
} }
s.setFlashMessage(c, "Changes applied successfully!", "success") SetFlashMessage(c, "Changes applied successfully!", "success")
if !s.config.WG.ManageIPAddresses { if !s.config.WG.ManageIPAddresses {
s.setFlashMessage(c, "WireGuard must be restarted to apply ip changes.", "warning") SetFlashMessage(c, "WireGuard must be restarted to apply ip changes.", "warning")
} }
c.Redirect(http.StatusSeeOther, "/admin/device/edit") c.Redirect(http.StatusSeeOther, "/admin/device/edit")
} }
func (s *Server) GetInterfaceConfig(c *gin.Context) { func (s *Server) GetInterfaceConfig(c *gin.Context) {
device := s.users.GetDevice() device := s.peers.GetDevice()
users := s.users.GetActiveUsers() users := s.peers.GetActivePeers()
cfg, err := device.GetConfigFile(users) cfg, err := device.GetConfigFile(users)
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
@ -122,19 +122,19 @@ func (s *Server) GetInterfaceConfig(c *gin.Context) {
} }
func (s *Server) GetApplyGlobalConfig(c *gin.Context) { func (s *Server) GetApplyGlobalConfig(c *gin.Context) {
device := s.users.GetDevice() device := s.peers.GetDevice()
users := s.users.GetAllUsers() users := s.peers.GetAllPeers()
for _, user := range users { for _, user := range users {
user.AllowedIPs = device.AllowedIPs user.AllowedIPs = device.AllowedIPs
user.AllowedIPsStr = device.AllowedIPsStr user.AllowedIPsStr = device.AllowedIPsStr
if err := s.users.UpdateUser(user); err != nil { if err := s.peers.UpdatePeer(user); err != nil {
s.setFlashMessage(c, err.Error(), "danger") SetFlashMessage(c, err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit") c.Redirect(http.StatusSeeOther, "/admin/device/edit")
} }
} }
s.setFlashMessage(c, "Allowed IP's updated for all clients.", "success") SetFlashMessage(c, "Allowed IP's updated for all clients.", "success")
c.Redirect(http.StatusSeeOther, "/admin/device/edit") c.Redirect(http.StatusSeeOther, "/admin/device/edit")
return return
} }

View File

@ -10,7 +10,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/ldap" "github.com/h44z/wg-portal/internal/users"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/tatsushid/go-fastping" "github.com/tatsushid/go-fastping"
) )
@ -21,10 +21,10 @@ type LdapCreateForm struct {
} }
func (s *Server) GetAdminEditPeer(c *gin.Context) { func (s *Server) GetAdminEditPeer(c *gin.Context) {
device := s.users.GetDevice() device := s.peers.GetDevice()
user := s.users.GetUserByKey(c.Query("pkey")) peer := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession, err := s.setFormInSession(c, user) currentSession, err := s.setFormInSession(c, peer)
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
return return
@ -40,7 +40,7 @@ func (s *Server) GetAdminEditPeer(c *gin.Context) {
EditableKeys bool EditableKeys bool
}{ }{
Route: c.Request.URL.Path, Route: c.Request.URL.Path,
Alerts: s.getFlashes(c), Alerts: GetFlashes(c),
Session: currentSession, Session: currentSession,
Static: s.getStaticData(), Static: s.getStaticData(),
Peer: currentSession.FormData.(Peer), Peer: currentSession.FormData.(Peer),
@ -50,17 +50,17 @@ func (s *Server) GetAdminEditPeer(c *gin.Context) {
} }
func (s *Server) PostAdminEditPeer(c *gin.Context) { func (s *Server) PostAdminEditPeer(c *gin.Context) {
currentUser := s.users.GetUserByKey(c.Query("pkey")) currentPeer := s.peers.GetPeerByKey(c.Query("pkey"))
urlEncodedKey := url.QueryEscape(c.Query("pkey")) urlEncodedKey := url.QueryEscape(c.Query("pkey"))
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
var formPeer Peer var formPeer Peer
if currentSession.FormData != nil { if currentSession.FormData != nil {
formPeer = currentSession.FormData.(Peer) formPeer = currentSession.FormData.(Peer)
} }
if err := c.ShouldBind(&formPeer); err != nil { if err := c.ShouldBind(&formPeer); err != nil {
_ = s.updateFormInSession(c, formPeer) _ = s.updateFormInSession(c, formPeer)
s.setFlashMessage(c, "failed to bind form data: "+err.Error(), "danger") SetFlashMessage(c, "failed to bind form data: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=bind") c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=bind")
return return
} }
@ -73,28 +73,28 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) {
disabled := c.PostForm("isdisabled") != "" disabled := c.PostForm("isdisabled") != ""
now := time.Now() now := time.Now()
if disabled && currentUser.DeactivatedAt == nil { if disabled && currentPeer.DeactivatedAt == nil {
formPeer.DeactivatedAt = &now formPeer.DeactivatedAt = &now
} else if !disabled { } else if !disabled {
formPeer.DeactivatedAt = nil formPeer.DeactivatedAt = nil
} }
// Update in database // Update in database
if err := s.UpdateUser(formPeer, now); err != nil { if err := s.UpdatePeer(formPeer, now); err != nil {
_ = s.updateFormInSession(c, formPeer) _ = s.updateFormInSession(c, formPeer)
s.setFlashMessage(c, "failed to update user: "+err.Error(), "danger") SetFlashMessage(c, "failed to update user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=update") c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=update")
return return
} }
s.setFlashMessage(c, "changes applied successfully", "success") SetFlashMessage(c, "changes applied successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey)
} }
func (s *Server) GetAdminCreatePeer(c *gin.Context) { func (s *Server) GetAdminCreatePeer(c *gin.Context) {
device := s.users.GetDevice() device := s.peers.GetDevice()
currentSession, err := s.setNewUserFormInSession(c) currentSession, err := s.setNewPeerFormInSession(c)
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
return return
@ -109,7 +109,7 @@ func (s *Server) GetAdminCreatePeer(c *gin.Context) {
EditableKeys bool EditableKeys bool
}{ }{
Route: c.Request.URL.Path, Route: c.Request.URL.Path,
Alerts: s.getFlashes(c), Alerts: GetFlashes(c),
Session: currentSession, Session: currentSession,
Static: s.getStaticData(), Static: s.getStaticData(),
Peer: currentSession.FormData.(Peer), Peer: currentSession.FormData.(Peer),
@ -119,14 +119,14 @@ func (s *Server) GetAdminCreatePeer(c *gin.Context) {
} }
func (s *Server) PostAdminCreatePeer(c *gin.Context) { func (s *Server) PostAdminCreatePeer(c *gin.Context) {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
var formPeer Peer var formPeer Peer
if currentSession.FormData != nil { if currentSession.FormData != nil {
formPeer = currentSession.FormData.(Peer) formPeer = currentSession.FormData.(Peer)
} }
if err := c.ShouldBind(&formPeer); err != nil { if err := c.ShouldBind(&formPeer); err != nil {
_ = s.updateFormInSession(c, formPeer) _ = s.updateFormInSession(c, formPeer)
s.setFlashMessage(c, "failed to bind form data: "+err.Error(), "danger") SetFlashMessage(c, "failed to bind form data: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=bind") c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=bind")
return return
} }
@ -143,14 +143,14 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) {
formPeer.DeactivatedAt = &now formPeer.DeactivatedAt = &now
} }
if err := s.CreateUser(formPeer); err != nil { if err := s.CreatePeer(formPeer); err != nil {
_ = s.updateFormInSession(c, formPeer) _ = s.updateFormInSession(c, formPeer)
s.setFlashMessage(c, "failed to add user: "+err.Error(), "danger") SetFlashMessage(c, "failed to add user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=create") c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=create")
return return
} }
s.setFlashMessage(c, "client created successfully", "success") SetFlashMessage(c, "client created successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin") c.Redirect(http.StatusSeeOther, "/admin")
} }
@ -166,29 +166,29 @@ func (s *Server) GetAdminCreateLdapPeers(c *gin.Context) {
Alerts []FlashData Alerts []FlashData
Session SessionData Session SessionData
Static StaticData Static StaticData
Users []*ldap.UserCacheHolderEntry Users []users.User
FormData LdapCreateForm FormData LdapCreateForm
Device Device Device Device
}{ }{
Route: c.Request.URL.Path, Route: c.Request.URL.Path,
Alerts: s.getFlashes(c), Alerts: GetFlashes(c),
Session: currentSession, Session: currentSession,
Static: s.getStaticData(), Static: s.getStaticData(),
Users: s.ldapUsers.GetSortedUsers("sn", "asc"), Users: s.users.GetFilteredAndSortedUsers("lastname", "asc", ""),
FormData: currentSession.FormData.(LdapCreateForm), FormData: currentSession.FormData.(LdapCreateForm),
Device: s.users.GetDevice(), Device: s.peers.GetDevice(),
}) })
} }
func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) { func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
var formData LdapCreateForm var formData LdapCreateForm
if currentSession.FormData != nil { if currentSession.FormData != nil {
formData = currentSession.FormData.(LdapCreateForm) formData = currentSession.FormData.(LdapCreateForm)
} }
if err := c.ShouldBind(&formData); err != nil { if err := c.ShouldBind(&formData); err != nil {
_ = s.updateFormInSession(c, formData) _ = s.updateFormInSession(c, formData)
s.setFlashMessage(c, "failed to bind form data: "+err.Error(), "danger") SetFlashMessage(c, "failed to bind form data: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=bind") c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=bind")
return return
} }
@ -196,9 +196,9 @@ func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
emails := common.ParseStringList(formData.Emails) emails := common.ParseStringList(formData.Emails)
for i := range emails { for i := range emails {
// TODO: also check email addr for validity? // TODO: also check email addr for validity?
if !strings.ContainsRune(emails[i], '@') || s.ldapUsers.GetUserDNByMail(emails[i]) == "" { if !strings.ContainsRune(emails[i], '@') || s.users.GetUser(emails[i]) == nil {
_ = s.updateFormInSession(c, formData) _ = s.updateFormInSession(c, formData)
s.setFlashMessage(c, "invalid email address: "+emails[i], "danger") SetFlashMessage(c, "invalid email address: "+emails[i], "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=mail") c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=mail")
return return
} }
@ -207,31 +207,31 @@ func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
logrus.Infof("creating %d ldap peers", len(emails)) logrus.Infof("creating %d ldap peers", len(emails))
for i := range emails { for i := range emails {
if err := s.CreateUserByEmail(emails[i], formData.Identifier, false); err != nil { if err := s.CreatePeerByEmail(emails[i], formData.Identifier, false); err != nil {
_ = s.updateFormInSession(c, formData) _ = s.updateFormInSession(c, formData)
s.setFlashMessage(c, "failed to add user: "+err.Error(), "danger") SetFlashMessage(c, "failed to add user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create") c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create")
return return
} }
} }
s.setFlashMessage(c, "client(s) created successfully", "success") SetFlashMessage(c, "client(s) created successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin/peer/createldap") c.Redirect(http.StatusSeeOther, "/admin/peer/createldap")
} }
func (s *Server) GetAdminDeletePeer(c *gin.Context) { func (s *Server) GetAdminDeletePeer(c *gin.Context) {
currentUser := s.users.GetUserByKey(c.Query("pkey")) currentUser := s.peers.GetPeerByKey(c.Query("pkey"))
if err := s.DeleteUser(currentUser); err != nil { if err := s.DeletePeer(currentUser); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Deletion error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "Deletion error", err.Error())
return return
} }
s.setFlashMessage(c, "user deleted successfully", "success") SetFlashMessage(c, "user deleted successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin") c.Redirect(http.StatusSeeOther, "/admin")
} }
func (s *Server) GetPeerQRCode(c *gin.Context) { func (s *Server) GetPeerQRCode(c *gin.Context) {
user := s.users.GetUserByKey(c.Query("pkey")) user := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email { if !currentSession.IsAdmin && user.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return return
@ -247,14 +247,14 @@ func (s *Server) GetPeerQRCode(c *gin.Context) {
} }
func (s *Server) GetPeerConfig(c *gin.Context) { func (s *Server) GetPeerConfig(c *gin.Context) {
user := s.users.GetUserByKey(c.Query("pkey")) user := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email { if !currentSession.IsAdmin && user.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return return
} }
cfg, err := user.GetConfigFile(s.users.GetDevice()) cfg, err := user.GetConfigFile(s.peers.GetDevice())
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return return
@ -266,14 +266,14 @@ func (s *Server) GetPeerConfig(c *gin.Context) {
} }
func (s *Server) GetPeerConfigMail(c *gin.Context) { func (s *Server) GetPeerConfigMail(c *gin.Context) {
user := s.users.GetUserByKey(c.Query("pkey")) user := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email { if !currentSession.IsAdmin && user.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return return
} }
cfg, err := user.GetConfigFile(s.users.GetDevice()) cfg, err := user.GetConfigFile(s.peers.GetDevice())
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return return
@ -319,13 +319,13 @@ func (s *Server) GetPeerConfigMail(c *gin.Context) {
return return
} }
s.setFlashMessage(c, "mail sent successfully", "success") SetFlashMessage(c, "mail sent successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin") c.Redirect(http.StatusSeeOther, "/admin")
} }
func (s *Server) GetPeerStatus(c *gin.Context) { func (s *Server) GetPeerStatus(c *gin.Context) {
user := s.users.GetUserByKey(c.Query("pkey")) user := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := s.getSessionData(c) currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email { if !currentSession.IsAdmin && user.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return return

View File

@ -0,0 +1,269 @@
package server
import (
"net/http"
"net/url"
"time"
"github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/users"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
func (s *Server) GetAdminUsersIndex(c *gin.Context) {
currentSession := GetSessionData(c)
sort := c.Query("sort")
if sort != "" {
if currentSession.SortedBy["users"] != sort {
currentSession.SortedBy["users"] = sort
currentSession.SortDirection["users"] = "asc"
} else {
if currentSession.SortDirection["users"] == "asc" {
currentSession.SortDirection["users"] = "desc"
} else {
currentSession.SortDirection["users"] = "asc"
}
}
if err := UpdateSessionData(c, currentSession); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "sort error", "failed to save session")
return
}
c.Redirect(http.StatusSeeOther, "/admin/users/")
return
}
search, searching := c.GetQuery("search")
if searching {
currentSession.Search["users"] = search
if err := UpdateSessionData(c, currentSession); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "search error", "failed to save session")
return
}
c.Redirect(http.StatusSeeOther, "/admin/users/")
return
}
dbUsers := s.users.GetFilteredAndSortedUsersUnscoped(currentSession.SortedBy["users"], currentSession.SortDirection["users"], currentSession.Search["users"])
c.HTML(http.StatusOK, "admin_user_index.html", struct {
Route string
Alerts []FlashData
Session SessionData
Static StaticData
Users []users.User
TotalUsers int
Device Device
}{
Route: c.Request.URL.Path,
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Users: dbUsers,
TotalUsers: len(s.users.GetUsers()),
Device: s.peers.GetDevice(),
})
}
func (s *Server) GetAdminUsersEdit(c *gin.Context) {
user := s.users.GetUserUnscoped(c.Query("pkey"))
currentSession, err := s.setFormInSession(c, *user)
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
return
}
c.HTML(http.StatusOK, "admin_edit_user.html", struct {
Route string
Alerts []FlashData
Session SessionData
Static StaticData
User users.User
Device Device
Epoch time.Time
}{
Route: c.Request.URL.Path,
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
User: currentSession.FormData.(users.User),
Device: s.peers.GetDevice(),
})
}
func (s *Server) PostAdminUsersEdit(c *gin.Context) {
currentUser := s.users.GetUserUnscoped(c.Query("pkey"))
if currentUser == nil {
SetFlashMessage(c, "invalid user", "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/")
return
}
urlEncodedKey := url.QueryEscape(c.Query("pkey"))
currentSession := GetSessionData(c)
var formUser users.User
if currentSession.FormData != nil {
formUser = currentSession.FormData.(users.User)
}
if err := c.ShouldBind(&formUser); err != nil {
_ = s.updateFormInSession(c, formUser)
SetFlashMessage(c, "failed to bind form data: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/edit?pkey="+urlEncodedKey+"&formerr=bind")
return
}
if formUser.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(formUser.Password), bcrypt.DefaultCost)
if err != nil {
_ = s.updateFormInSession(c, formUser)
SetFlashMessage(c, "failed to hash admin password", "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/edit?pkey="+urlEncodedKey+"&formerr=bind")
return
}
formUser.Password = string(hashedPassword)
} else {
formUser.Password = currentUser.Password
}
disabled := c.PostForm("isdisabled") != ""
if disabled {
formUser.DeletedAt = gorm.DeletedAt{
Time: time.Now(),
Valid: true,
}
} else {
formUser.DeletedAt = gorm.DeletedAt{}
}
formUser.IsAdmin = c.PostForm("isadmin") == "true"
// Update peers
if disabled != currentUser.DeletedAt.Valid {
if disabled {
// disable all peers for the given user
for _, peer := range s.peers.GetPeersByMail(currentUser.Email) {
now := time.Now()
peer.DeactivatedAt = &now
if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update deactivated peer %s: %v", peer.PublicKey, err)
}
}
} else {
// enable all peers for the given user
for _, peer := range s.peers.GetPeersByMail(currentUser.Email) {
now := time.Now()
peer.DeactivatedAt = nil
if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update activated peer %s: %v", peer.PublicKey, err)
}
}
}
}
// Update in database
if err := s.users.UpdateUser(&formUser); err != nil {
_ = s.updateFormInSession(c, formUser)
SetFlashMessage(c, "failed to update user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/edit?pkey="+urlEncodedKey+"&formerr=update")
return
}
SetFlashMessage(c, "changes applied successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin/users/edit?pkey="+urlEncodedKey)
}
func (s *Server) GetAdminUsersCreate(c *gin.Context) {
user := users.User{}
currentSession, err := s.setFormInSession(c, user)
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
return
}
c.HTML(http.StatusOK, "admin_edit_user.html", struct {
Route string
Alerts []FlashData
Session SessionData
Static StaticData
User users.User
Device Device
Epoch time.Time
}{
Route: c.Request.URL.Path,
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
User: currentSession.FormData.(users.User),
Device: s.peers.GetDevice(),
})
}
func (s *Server) PostAdminUsersCreate(c *gin.Context) {
currentSession := GetSessionData(c)
var formUser users.User
if currentSession.FormData != nil {
formUser = currentSession.FormData.(users.User)
}
if err := c.ShouldBind(&formUser); err != nil {
_ = s.updateFormInSession(c, formUser)
SetFlashMessage(c, "failed to bind form data: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=bind")
return
}
if formUser.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(formUser.Password), bcrypt.DefaultCost)
if err != nil {
SetFlashMessage(c, "failed to hash admin password", "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=bind")
return
}
formUser.Password = string(hashedPassword)
} else {
_ = s.updateFormInSession(c, formUser)
SetFlashMessage(c, "invalid password", "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create")
return
}
disabled := c.PostForm("isdisabled") != ""
if disabled {
formUser.DeletedAt = gorm.DeletedAt{
Time: time.Now(),
Valid: true,
}
} else {
formUser.DeletedAt = gorm.DeletedAt{}
}
formUser.IsAdmin = c.PostForm("isadmin") == "true"
formUser.Source = users.UserSourceDatabase
if err := s.users.CreateUser(&formUser); err != nil {
formUser.CreatedAt = time.Time{} // reset created time
_ = s.updateFormInSession(c, formUser)
SetFlashMessage(c, "failed to add user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create")
return
}
// Check if user already has a peer setup, if not create one
if s.config.Core.CreateDefaultPeer {
peers := s.peers.GetPeersByMail(formUser.Email)
if len(peers) == 0 { // Create vpn peer
err := s.CreatePeer(Peer{
Identifier: formUser.Firstname + " " + formUser.Lastname + " (Default)",
Email: formUser.Email,
CreatedBy: formUser.Email,
UpdatedBy: formUser.Email,
})
logrus.Errorf("Failed to automatically create vpn peer for %s: %v", formUser.Email, err)
}
}
SetFlashMessage(c, "user created successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin/users/")
}

View File

@ -2,25 +2,25 @@ package server
import ( import (
"crypto/md5" "crypto/md5"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"syscall" "syscall"
"time" "time"
"github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/common"
"github.com/pkg/errors"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
func (s *Server) PrepareNewUser() (Peer, error) { func (s *Server) PrepareNewPeer() (Peer, error) {
device := s.users.GetDevice() device := s.peers.GetDevice()
peer := Peer{} peer := Peer{}
peer.IsNew = true peer.IsNew = true
peer.AllowedIPsStr = device.AllowedIPsStr peer.AllowedIPsStr = device.AllowedIPsStr
peer.IPs = make([]string, len(device.IPs)) peer.IPs = make([]string, len(device.IPs))
for i := range device.IPs { for i := range device.IPs {
freeIP, err := s.users.GetAvailableIp(device.IPs[i]) freeIP, err := s.peers.GetAvailableIp(device.IPs[i])
if err != nil { if err != nil {
return Peer{}, err return Peer{}, err
} }
@ -43,18 +43,19 @@ func (s *Server) PrepareNewUser() (Peer, error) {
return peer, nil return peer, nil
} }
func (s *Server) CreateUserByEmail(email, identifierSuffix string, disabled bool) error { func (s *Server) CreatePeerByEmail(email, identifierSuffix string, disabled bool) error {
ldapUser := s.ldapUsers.GetUserData(s.ldapUsers.GetUserDNByMail(email)) user, err := s.users.GetOrCreateUser(email)
if ldapUser.DN == "" { if err != nil {
return errors.New("no peer with email " + email + " found") return errors.WithMessagef(err, "failed to load/create related user %s", email)
} }
device := s.users.GetDevice() device := s.peers.GetDevice()
peer := Peer{} peer := Peer{}
peer.User = user
peer.AllowedIPsStr = device.AllowedIPsStr peer.AllowedIPsStr = device.AllowedIPsStr
peer.IPs = make([]string, len(device.IPs)) peer.IPs = make([]string, len(device.IPs))
for i := range device.IPs { for i := range device.IPs {
freeIP, err := s.users.GetAvailableIp(device.IPs[i]) freeIP, err := s.peers.GetAvailableIp(device.IPs[i])
if err != nil { if err != nil {
return err return err
} }
@ -74,30 +75,30 @@ func (s *Server) CreateUserByEmail(email, identifierSuffix string, disabled bool
peer.PublicKey = key.PublicKey().String() peer.PublicKey = key.PublicKey().String()
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey))) peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
peer.Email = email peer.Email = email
peer.Identifier = fmt.Sprintf("%s %s (%s)", ldapUser.Firstname, ldapUser.Lastname, identifierSuffix) peer.Identifier = fmt.Sprintf("%s %s (%s)", user.Firstname, user.Lastname, identifierSuffix)
now := time.Now() now := time.Now()
if disabled { if disabled {
peer.DeactivatedAt = &now peer.DeactivatedAt = &now
} }
return s.CreateUser(peer) return s.CreatePeer(peer)
} }
func (s *Server) CreateUser(user Peer) error { func (s *Server) CreatePeer(peer Peer) error {
device := s.peers.GetDevice()
device := s.users.GetDevice() peer.AllowedIPsStr = device.AllowedIPsStr
user.AllowedIPsStr = device.AllowedIPsStr if peer.IPs == nil || len(peer.IPs) == 0 {
if len(user.IPs) == 0 { peer.IPs = make([]string, len(device.IPs))
for i := range device.IPs { for i := range device.IPs {
freeIP, err := s.users.GetAvailableIp(device.IPs[i]) freeIP, err := s.peers.GetAvailableIp(device.IPs[i])
if err != nil { if err != nil {
return err return err
} }
user.IPs[i] = freeIP peer.IPs[i] = freeIP
} }
user.IPsStr = common.ListToString(user.IPs) peer.IPsStr = common.ListToString(peer.IPs)
} }
if user.PrivateKey == "" { // if private key is empty create a new one if peer.PrivateKey == "" { // if private key is empty create a new one
psk, err := wgtypes.GenerateKey() psk, err := wgtypes.GenerateKey()
if err != nil { if err != nil {
return err return err
@ -106,60 +107,60 @@ func (s *Server) CreateUser(user Peer) error {
if err != nil { if err != nil {
return err return err
} }
user.PresharedKey = psk.String() peer.PresharedKey = psk.String()
user.PrivateKey = key.String() peer.PrivateKey = key.String()
user.PublicKey = key.PublicKey().String() peer.PublicKey = key.PublicKey().String()
} }
user.UID = fmt.Sprintf("u%x", md5.Sum([]byte(user.PublicKey))) peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
// Create WireGuard interface // Create WireGuard interface
if user.DeactivatedAt == nil { if peer.DeactivatedAt == nil {
if err := s.wg.AddPeer(user.GetConfig()); err != nil { if err := s.wg.AddPeer(peer.GetConfig()); err != nil {
return err return err
} }
} }
// Create in database // Create in database
if err := s.users.CreateUser(user); err != nil { if err := s.peers.CreatePeer(peer); err != nil {
return err return err
} }
return s.WriteWireGuardConfigFile() return s.WriteWireGuardConfigFile()
} }
func (s *Server) UpdateUser(user Peer, updateTime time.Time) error { func (s *Server) UpdatePeer(peer Peer, updateTime time.Time) error {
currentUser := s.users.GetUserByKey(user.PublicKey) currentPeer := s.peers.GetPeerByKey(peer.PublicKey)
// Update WireGuard device // Update WireGuard device
var err error var err error
switch { switch {
case user.DeactivatedAt == &updateTime: case peer.DeactivatedAt == &updateTime:
err = s.wg.RemovePeer(user.PublicKey) err = s.wg.RemovePeer(peer.PublicKey)
case user.DeactivatedAt == nil && currentUser.Peer != nil: case peer.DeactivatedAt == nil && currentPeer.Peer != nil:
err = s.wg.UpdatePeer(user.GetConfig()) err = s.wg.UpdatePeer(peer.GetConfig())
case user.DeactivatedAt == nil && currentUser.Peer == nil: case peer.DeactivatedAt == nil && currentPeer.Peer == nil:
err = s.wg.AddPeer(user.GetConfig()) err = s.wg.AddPeer(peer.GetConfig())
} }
if err != nil { if err != nil {
return err return err
} }
// Update in database // Update in database
if err := s.users.UpdateUser(user); err != nil { if err := s.peers.UpdatePeer(peer); err != nil {
return err return err
} }
return s.WriteWireGuardConfigFile() return s.WriteWireGuardConfigFile()
} }
func (s *Server) DeleteUser(user Peer) error { func (s *Server) DeletePeer(peer Peer) error {
// Delete WireGuard peer // Delete WireGuard peer
if err := s.wg.RemovePeer(user.PublicKey); err != nil { if err := s.wg.RemovePeer(peer.PublicKey); err != nil {
return err return err
} }
// Delete in database // Delete in database
if err := s.users.DeleteUser(user); err != nil { if err := s.peers.DeletePeer(peer); err != nil {
return err return err
} }
@ -167,11 +168,11 @@ func (s *Server) DeleteUser(user Peer) error {
} }
func (s *Server) RestoreWireGuardInterface() error { func (s *Server) RestoreWireGuardInterface() error {
activeUsers := s.users.GetActiveUsers() activePeers := s.peers.GetActivePeers()
for i := range activeUsers { for i := range activePeers {
if activeUsers[i].Peer == nil { if activePeers[i].Peer == nil {
if err := s.wg.AddPeer(activeUsers[i].GetConfig()); err != nil { if err := s.wg.AddPeer(activePeers[i].GetConfig()); err != nil {
return err return err
} }
} }
@ -188,8 +189,8 @@ func (s *Server) WriteWireGuardConfigFile() error {
return err return err
} }
device := s.users.GetDevice() device := s.peers.GetDevice()
cfg, err := device.GetConfigFile(s.users.GetActiveUsers()) cfg, err := device.GetConfigFile(s.peers.GetActivePeers())
if err != nil { if err != nil {
return err return err
} }

View File

@ -4,31 +4,147 @@ import (
"time" "time"
"github.com/h44z/wg-portal/internal/ldap" "github.com/h44z/wg-portal/internal/ldap"
"github.com/h44z/wg-portal/internal/users"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm"
) )
// SyncLdapAttributesWithWireGuard starts to synchronize the "disabled" attribute from ldap. func (s *Server) SyncLdapWithUserDatabase() {
// Users will be automatically disabled once they are disabled in ldap. logrus.Info("starting ldap user synchronization...")
// This method is blocking. running := true
func (s *Server) SyncLdapAttributesWithWireGuard() error { for running {
allUsers := s.users.GetAllUsers() // Select blocks until one of the cases happens
for i := range allUsers { select {
user := allUsers[i] case <-time.After(1 * time.Minute):
if user.LdapUser == nil { // Sleep for 1 minute
continue // skip non ldap users case <-s.ctx.Done():
logrus.Trace("ldap-sync shutting down (context ended)...")
running = false
continue
} }
if user.DeactivatedAt != nil { // Main work here
continue // skip already disabled interfaces logrus.Trace("syncing ldap users to database...")
ldapUsers, err := ldap.FindAllUsers(&s.config.LDAP)
if err != nil {
logrus.Errorf("failed to fetch users from ldap: %v", err)
continue
} }
if ldap.IsLdapUserDisabled(allUsers[i].LdapUser.Attributes["userAccountControl"]) { for i := range ldapUsers {
// prefilter
if ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute] == "" ||
ldapUsers[i].Attributes[s.config.LDAP.FirstNameAttribute] == "" ||
ldapUsers[i].Attributes[s.config.LDAP.LastNameAttribute] == "" {
continue
}
user, err := s.users.GetOrCreateUserUnscoped(ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute])
if err != nil {
logrus.Errorf("failed to get/create user %s in database: %v", ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute], err)
}
// check if user should be deactivated
ldapDeactivated := false
switch s.config.LDAP.Type {
case ldap.TypeActiveDirectory:
ldapDeactivated = ldap.IsActiveDirectoryUserDisabled(ldapUsers[i].Attributes[s.config.LDAP.DisabledAttribute])
case ldap.TypeOpenLDAP:
ldapDeactivated = ldap.IsOpenLdapUserDisabled(ldapUsers[i].Attributes[s.config.LDAP.DisabledAttribute])
}
// check if user has been disabled in ldap, update peers accordingly
if ldapDeactivated != user.DeletedAt.Valid {
if ldapDeactivated {
// disable all peers for the given user
for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now() now := time.Now()
user.DeactivatedAt = &now peer.DeactivatedAt = &now
if err := s.UpdateUser(user, now); err != nil { if err = s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("Failed to disable user %s: %v", user.Email, err) logrus.Errorf("failed to update deactivated peer %s: %v", peer.PublicKey, err)
}
}
} else {
// enable all peers for the given user
for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now()
peer.DeactivatedAt = nil
if err = s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update activated peer %s: %v", peer.PublicKey, err)
} }
} }
} }
return nil }
// Sync attributes from ldap
if s.UserChangedInLdap(user, &ldapUsers[i]) {
user.Firstname = ldapUsers[i].Attributes[s.config.LDAP.FirstNameAttribute]
user.Lastname = ldapUsers[i].Attributes[s.config.LDAP.LastNameAttribute]
user.Email = ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute]
user.Phone = ldapUsers[i].Attributes[s.config.LDAP.PhoneAttribute]
user.IsAdmin = false
user.Source = users.UserSourceLdap
user.DeletedAt = gorm.DeletedAt{} // Not deleted
for _, group := range ldapUsers[i].RawAttributes[s.config.LDAP.GroupMemberAttribute] {
if string(group) == s.config.LDAP.AdminLdapGroup {
user.IsAdmin = true
break
}
}
if ldapDeactivated {
if err = s.users.DeleteUser(user); err != nil {
logrus.Errorf("failed to delete deactivated user %s in database: %v", user.Email, err)
continue
}
} else {
if err = s.users.UpdateUser(user); err != nil {
logrus.Errorf("failed to update ldap user %s in database: %v", user.Email, err)
continue
}
}
}
}
}
logrus.Info("ldap user synchronization stopped")
}
func (s Server) UserChangedInLdap(user *users.User, ldapData *ldap.RawLdapData) bool {
if user.Firstname != ldapData.Attributes[s.config.LDAP.FirstNameAttribute] {
return true
}
if user.Lastname != ldapData.Attributes[s.config.LDAP.LastNameAttribute] {
return true
}
if user.Email != ldapData.Attributes[s.config.LDAP.EmailAttribute] {
return true
}
if user.Phone != ldapData.Attributes[s.config.LDAP.PhoneAttribute] {
return true
}
ldapDeactivated := false
switch s.config.LDAP.Type {
case ldap.TypeActiveDirectory:
ldapDeactivated = ldap.IsActiveDirectoryUserDisabled(ldapData.Attributes[s.config.LDAP.DisabledAttribute])
case ldap.TypeOpenLDAP:
ldapDeactivated = ldap.IsOpenLdapUserDisabled(ldapData.Attributes[s.config.LDAP.DisabledAttribute])
}
if ldapDeactivated != user.DeletedAt.Valid {
return true
}
ldapAdmin := false
for _, group := range ldapData.RawAttributes[s.config.LDAP.GroupMemberAttribute] {
if string(group) == s.config.LDAP.AdminLdapGroup {
ldapAdmin = true
break
}
}
if user.IsAdmin != ldapAdmin {
return true
}
return false
} }

View File

@ -3,11 +3,8 @@ package server
import ( import (
"bytes" "bytes"
"crypto/md5" "crypto/md5"
"errors"
"fmt" "fmt"
"net" "net"
"os"
"path/filepath"
"reflect" "reflect"
"regexp" "regexp"
"sort" "sort"
@ -18,12 +15,12 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/ldap" "github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard" "github.com/h44z/wg-portal/internal/wireguard"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -68,8 +65,8 @@ func init() {
// //
type Peer struct { type Peer struct {
Peer *wgtypes.Peer `gorm:"-"` Peer *wgtypes.Peer `gorm:"-"` // WireGuard peer
LdapUser *ldap.UserCacheHolderEntry `gorm:"-"` // optional, it is still possible to have users without ldap User *users.User `gorm:"-"` // user reference for the peer
Config string `gorm:"-"` Config string `gorm:"-"`
UID string `form:"uid" binding:"alphanum"` // uid for html identification UID string `form:"uid" binding:"alphanum"` // uid for html identification
@ -247,7 +244,7 @@ func (d Device) GetConfig() wgtypes.Config {
return cfg return cfg
} }
func (d Device) GetConfigFile(clients []Peer) ([]byte, error) { func (d Device) GetConfigFile(peers []Peer) ([]byte, error) {
tpl, err := template.New("server").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(wireguard.DeviceCfgTpl) tpl, err := template.New("server").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(wireguard.DeviceCfgTpl)
if err != nil { if err != nil {
return nil, err return nil, err
@ -259,7 +256,7 @@ func (d Device) GetConfigFile(clients []Peer) ([]byte, error) {
Clients []Peer Clients []Peer
Server Device Server Device
}{ }{
Clients: clients, Clients: peers,
Server: d, Server: d,
}) })
if err != nil { if err != nil {
@ -270,78 +267,65 @@ func (d Device) GetConfigFile(clients []Peer) ([]byte, error) {
} }
// //
// USER-MANAGER -------------------------------------------------------------------------------- // PEER-MANAGER --------------------------------------------------------------------------------
// //
type UserManager struct { type PeerManager struct {
db *gorm.DB db *gorm.DB
wg *wireguard.Manager wg *wireguard.Manager
ldapUsers *ldap.SynchronizedUserCacheHolder users *users.Manager
} }
func NewUserManager(dbPath string, wg *wireguard.Manager, ldapUsers *ldap.SynchronizedUserCacheHolder) *UserManager { func NewPeerManager(cfg *common.Config, wg *wireguard.Manager, userDB *users.Manager) (*PeerManager, error) {
um := &PeerManager{wg: wg, users: userDB}
um := &UserManager{wg: wg, ldapUsers: ldapUsers}
var err error var err error
if _, err = os.Stat(filepath.Dir(dbPath)); os.IsNotExist(err) { um.db, err = users.GetDatabaseForConfig(&cfg.Database)
if err = os.MkdirAll(filepath.Dir(dbPath), 0700); err != nil {
logrus.Errorf("failed to create database directory (%s): %v", filepath.Dir(dbPath), err)
return nil
}
}
um.db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil { if err != nil {
logrus.Errorf("failed to open sqlite database (%s): %v", dbPath, err) return nil, errors.WithMessage(err, "failed to open peer database")
return nil
} }
err = um.db.AutoMigrate(&Peer{}, &Device{}) err = um.db.AutoMigrate(&Peer{}, &Device{})
if err != nil { if err != nil {
logrus.Errorf("failed to migrate sqlite database: %v", err) return nil, errors.WithMessage(err, "failed to migrate peer database")
return nil
} }
return um return um, nil
} }
func (u *UserManager) InitFromCurrentInterface() error { func (u *PeerManager) InitFromCurrentInterface() error {
peers, err := u.wg.GetPeerList() peers, err := u.wg.GetPeerList()
if err != nil { if err != nil {
logrus.Errorf("failed to init user-manager from peers: %v", err) return errors.Wrapf(err, "failed to get peer list")
return err
} }
device, err := u.wg.GetDeviceInfo() device, err := u.wg.GetDeviceInfo()
if err != nil { if err != nil {
logrus.Errorf("failed to init user-manager from device: %v", err) return errors.Wrapf(err, "failed to get device info")
return err
} }
var ipAddresses []string var ipAddresses []string
var mtu int var mtu int
if u.wg.Cfg.ManageIPAddresses { if u.wg.Cfg.ManageIPAddresses {
if ipAddresses, err = u.wg.GetIPAddress(); err != nil { if ipAddresses, err = u.wg.GetIPAddress(); err != nil {
logrus.Errorf("failed to init user-manager from device: %v", err) return errors.Wrapf(err, "failed to get ip address")
return err
} }
if mtu, err = u.wg.GetMTU(); err != nil { if mtu, err = u.wg.GetMTU(); err != nil {
logrus.Errorf("failed to init user-manager from device: %v", err) return errors.Wrapf(err, "failed to get MTU")
return err
} }
} }
// Check if entries already exist in database, if not create them // Check if entries already exist in database, if not create them
for _, peer := range peers { for _, peer := range peers {
if err := u.validateOrCreateUserForPeer(peer); err != nil { if err := u.validateOrCreatePeer(peer); err != nil {
return err return errors.WithMessagef(err, "failed to validate peer %s", peer.PublicKey)
} }
} }
if err := u.validateOrCreateDevice(*device, ipAddresses, mtu); err != nil { if err := u.validateOrCreateDevice(*device, ipAddresses, mtu); err != nil {
return err return errors.WithMessagef(err, "failed to validate device %s", device.Name)
} }
return nil return nil
} }
func (u *UserManager) validateOrCreateUserForPeer(wgPeer wgtypes.Peer) error { func (u *PeerManager) validateOrCreatePeer(wgPeer wgtypes.Peer) error {
peer := Peer{} peer := Peer{}
u.db.Where("public_key = ?", wgPeer.PublicKey.String()).FirstOrInit(&peer) u.db.Where("public_key = ?", wgPeer.PublicKey.String()).FirstOrInit(&peer)
@ -366,15 +350,14 @@ func (u *UserManager) validateOrCreateUserForPeer(wgPeer wgtypes.Peer) error {
res := u.db.Create(&peer) res := u.db.Create(&peer)
if res.Error != nil { if res.Error != nil {
logrus.Errorf("failed to create autodetected wgPeer: %v", res.Error) return errors.Wrapf(res.Error, "failed to create autodetected peer %s", peer.PublicKey)
return res.Error
} }
} }
return nil return nil
} }
func (u *UserManager) validateOrCreateDevice(dev wgtypes.Device, ipAddresses []string, mtu int) error { func (u *PeerManager) validateOrCreateDevice(dev wgtypes.Device, ipAddresses []string, mtu int) error {
device := Device{} device := Device{}
u.db.Where("device_name = ?", dev.Name).FirstOrInit(&device) u.db.Where("device_name = ?", dev.Name).FirstOrInit(&device)
@ -393,47 +376,46 @@ func (u *UserManager) validateOrCreateDevice(dev wgtypes.Device, ipAddresses []s
res := u.db.Create(&device) res := u.db.Create(&device)
if res.Error != nil { if res.Error != nil {
logrus.Errorf("failed to create autodetected device: %v", res.Error) return errors.Wrapf(res.Error, "failed to create autodetected device")
return res.Error
} }
} }
return nil return nil
} }
func (u *UserManager) populateUserData(user *Peer) { func (u *PeerManager) populatePeerData(peer *Peer) {
user.AllowedIPs = strings.Split(user.AllowedIPsStr, ", ") peer.AllowedIPs = strings.Split(peer.AllowedIPsStr, ", ")
user.IPs = strings.Split(user.IPsStr, ", ") peer.IPs = strings.Split(peer.IPsStr, ", ")
// Set config file // Set config file
tmpCfg, _ := user.GetConfigFile(u.GetDevice()) tmpCfg, _ := peer.GetConfigFile(u.GetDevice())
user.Config = string(tmpCfg) peer.Config = string(tmpCfg)
// set data from WireGuard interface // set data from WireGuard interface
user.Peer, _ = u.wg.GetPeer(user.PublicKey) peer.Peer, _ = u.wg.GetPeer(peer.PublicKey)
user.LastHandshake = "never" peer.LastHandshake = "never"
user.LastHandshakeTime = "Never connected, or user is disabled." peer.LastHandshakeTime = "Never connected, or user is disabled."
if user.Peer != nil { if peer.Peer != nil {
since := time.Since(user.Peer.LastHandshakeTime) since := time.Since(peer.Peer.LastHandshakeTime)
sinceSeconds := int(since.Round(time.Second).Seconds()) sinceSeconds := int(since.Round(time.Second).Seconds())
sinceMinutes := int(sinceSeconds / 60) sinceMinutes := int(sinceSeconds / 60)
sinceSeconds -= sinceMinutes * 60 sinceSeconds -= sinceMinutes * 60
if sinceMinutes > 2*10080 { // 2 weeks if sinceMinutes > 2*10080 { // 2 weeks
user.LastHandshake = "a while ago" peer.LastHandshake = "a while ago"
} else if sinceMinutes > 10080 { // 1 week } else if sinceMinutes > 10080 { // 1 week
user.LastHandshake = "a week ago" peer.LastHandshake = "a week ago"
} else { } else {
user.LastHandshake = fmt.Sprintf("%02dm %02ds", sinceMinutes, sinceSeconds) peer.LastHandshake = fmt.Sprintf("%02dm %02ds", sinceMinutes, sinceSeconds)
} }
user.LastHandshakeTime = user.Peer.LastHandshakeTime.Format(time.UnixDate) peer.LastHandshakeTime = peer.Peer.LastHandshakeTime.Format(time.UnixDate)
} }
user.IsOnline = false // todo: calculate online status peer.IsOnline = false
// set ldap data // set user data
user.LdapUser = u.ldapUsers.GetUserData(u.ldapUsers.GetUserDNByMail(user.Email)) peer.User = u.users.GetUser(peer.Email)
} }
func (u *UserManager) populateDeviceData(device *Device) { func (u *PeerManager) populateDeviceData(device *Device) {
device.AllowedIPs = strings.Split(device.AllowedIPsStr, ", ") device.AllowedIPs = strings.Split(device.AllowedIPsStr, ", ")
device.IPs = strings.Split(device.IPsStr, ", ") device.IPs = strings.Split(device.IPsStr, ", ")
device.DNS = strings.Split(device.DNSStr, ", ") device.DNS = strings.Split(device.DNSStr, ", ")
@ -442,35 +424,35 @@ func (u *UserManager) populateDeviceData(device *Device) {
device.Interface, _ = u.wg.GetDeviceInfo() device.Interface, _ = u.wg.GetDeviceInfo()
} }
func (u *UserManager) GetAllUsers() []Peer { func (u *PeerManager) GetAllPeers() []Peer {
peers := make([]Peer, 0) peers := make([]Peer, 0)
u.db.Find(&peers) u.db.Find(&peers)
for i := range peers { for i := range peers {
u.populateUserData(&peers[i]) u.populatePeerData(&peers[i])
} }
return peers return peers
} }
func (u *UserManager) GetActiveUsers() []Peer { func (u *PeerManager) GetActivePeers() []Peer {
peers := make([]Peer, 0) peers := make([]Peer, 0)
u.db.Where("deactivated_at IS NULL").Find(&peers) u.db.Where("deactivated_at IS NULL").Find(&peers)
for i := range peers { for i := range peers {
u.populateUserData(&peers[i]) u.populatePeerData(&peers[i])
} }
return peers return peers
} }
func (u *UserManager) GetFilteredAndSortedUsers(sortKey, sortDirection, search string) []Peer { func (u *PeerManager) GetFilteredAndSortedPeers(sortKey, sortDirection, search string) []Peer {
peers := make([]Peer, 0) peers := make([]Peer, 0)
u.db.Find(&peers) u.db.Find(&peers)
filteredPeers := make([]Peer, 0, len(peers)) filteredPeers := make([]Peer, 0, len(peers))
for i := range peers { for i := range peers {
u.populateUserData(&peers[i]) u.populatePeerData(&peers[i])
if search == "" || if search == "" ||
strings.Contains(peers[i].Email, search) || strings.Contains(peers[i].Email, search) ||
@ -517,12 +499,12 @@ func (u *UserManager) GetFilteredAndSortedUsers(sortKey, sortDirection, search s
return filteredPeers return filteredPeers
} }
func (u *UserManager) GetSortedUsersForEmail(sortKey, sortDirection, email string) []Peer { func (u *PeerManager) GetSortedPeersForEmail(sortKey, sortDirection, email string) []Peer {
peers := make([]Peer, 0) peers := make([]Peer, 0)
u.db.Where("email = ?", email).Find(&peers) u.db.Where("email = ?", email).Find(&peers)
for i := range peers { for i := range peers {
u.populateUserData(&peers[i]) u.populatePeerData(&peers[i])
} }
sort.Slice(peers, func(i, j int) bool { sort.Slice(peers, func(i, j int) bool {
@ -562,7 +544,7 @@ func (u *UserManager) GetSortedUsersForEmail(sortKey, sortDirection, email strin
return peers return peers
} }
func (u *UserManager) GetDevice() Device { func (u *PeerManager) GetDevice() Device {
devices := make([]Device, 0, 1) devices := make([]Device, 0, 1)
u.db.Find(&devices) u.db.Find(&devices)
@ -573,24 +555,24 @@ func (u *UserManager) GetDevice() Device {
return devices[0] // use first device for now... more to come? return devices[0] // use first device for now... more to come?
} }
func (u *UserManager) GetUserByKey(publicKey string) Peer { func (u *PeerManager) GetPeerByKey(publicKey string) Peer {
peer := Peer{} peer := Peer{}
u.db.Where("public_key = ?", publicKey).FirstOrInit(&peer) u.db.Where("public_key = ?", publicKey).FirstOrInit(&peer)
u.populateUserData(&peer) u.populatePeerData(&peer)
return peer return peer
} }
func (u *UserManager) GetUsersByMail(mail string) []Peer { func (u *PeerManager) GetPeersByMail(mail string) []Peer {
var peers []Peer var peers []Peer
u.db.Where("email = ?", mail).Find(&peers) u.db.Where("email = ?", mail).Find(&peers)
for i := range peers { for i := range peers {
u.populateUserData(&peers[i]) u.populatePeerData(&peers[i])
} }
return peers return peers
} }
func (u *UserManager) CreateUser(peer Peer) error { func (u *PeerManager) CreatePeer(peer Peer) error {
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey))) peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
peer.UpdatedAt = time.Now() peer.UpdatedAt = time.Now()
peer.CreatedAt = time.Now() peer.CreatedAt = time.Now()
@ -606,7 +588,7 @@ func (u *UserManager) CreateUser(peer Peer) error {
return nil return nil
} }
func (u *UserManager) UpdateUser(peer Peer) error { func (u *PeerManager) UpdatePeer(peer Peer) error {
peer.UpdatedAt = time.Now() peer.UpdatedAt = time.Now()
peer.AllowedIPsStr = strings.Join(peer.AllowedIPs, ", ") peer.AllowedIPsStr = strings.Join(peer.AllowedIPs, ", ")
peer.IPsStr = strings.Join(peer.IPs, ", ") peer.IPsStr = strings.Join(peer.IPs, ", ")
@ -620,7 +602,7 @@ func (u *UserManager) UpdateUser(peer Peer) error {
return nil return nil
} }
func (u *UserManager) DeleteUser(peer Peer) error { func (u *PeerManager) DeletePeer(peer Peer) error {
res := u.db.Delete(&peer) res := u.db.Delete(&peer)
if res.Error != nil { if res.Error != nil {
logrus.Errorf("failed to delete peer: %v", res.Error) logrus.Errorf("failed to delete peer: %v", res.Error)
@ -630,7 +612,7 @@ func (u *UserManager) DeleteUser(peer Peer) error {
return nil return nil
} }
func (u *UserManager) UpdateDevice(device Device) error { func (u *PeerManager) UpdateDevice(device Device) error {
device.UpdatedAt = time.Now() device.UpdatedAt = time.Now()
device.AllowedIPsStr = strings.Join(device.AllowedIPs, ", ") device.AllowedIPsStr = strings.Join(device.AllowedIPs, ", ")
device.IPsStr = strings.Join(device.IPs, ", ") device.IPsStr = strings.Join(device.IPs, ", ")
@ -645,10 +627,10 @@ func (u *UserManager) UpdateDevice(device Device) error {
return nil return nil
} }
func (u *UserManager) GetAllReservedIps() ([]string, error) { func (u *PeerManager) GetAllReservedIps() ([]string, error) {
reservedIps := make([]string, 0) reservedIps := make([]string, 0)
users := u.GetAllUsers() peers := u.GetAllPeers()
for _, user := range users { for _, user := range peers {
for _, cidr := range user.IPs { for _, cidr := range user.IPs {
if cidr == "" { if cidr == "" {
continue continue
@ -677,7 +659,7 @@ func (u *UserManager) GetAllReservedIps() ([]string, error) {
return reservedIps, nil return reservedIps, nil
} }
func (u *UserManager) IsIPReserved(cidr string) bool { func (u *PeerManager) IsIPReserved(cidr string) bool {
reserved, err := u.GetAllReservedIps() reserved, err := u.GetAllReservedIps()
if err != nil { if err != nil {
return true // in case something failed, assume the ip is reserved return true // in case something failed, assume the ip is reserved
@ -706,7 +688,7 @@ func (u *UserManager) IsIPReserved(cidr string) bool {
} }
// GetAvailableIp search for an available ip in cidr against a list of reserved ips // GetAvailableIp search for an available ip in cidr against a list of reserved ips
func (u *UserManager) GetAvailableIp(cidr string) (string, error) { func (u *PeerManager) GetAvailableIp(cidr string) (string, error) {
reserved, err := u.GetAllReservedIps() reserved, err := u.GetAllReservedIps()
if err != nil { if err != nil {
return "", err return "", err

View File

@ -4,11 +4,20 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
wg_portal "github.com/h44z/wg-portal"
) )
func SetupRoutes(s *Server) { func SetupRoutes(s *Server) {
// Startpage // Startpage
s.server.GET("/", s.GetIndex) s.server.GET("/", s.GetIndex)
s.server.GET("/favicon.ico", func(c *gin.Context) {
file, _ := wg_portal.Statics.ReadFile("assets/img/favicon.ico")
c.Data(
http.StatusOK,
"image/x-icon",
file,
)
})
// Auth routes // Auth routes
auth := s.server.Group("/auth") auth := s.server.Group("/auth")
@ -18,7 +27,7 @@ func SetupRoutes(s *Server) {
// Admin routes // Admin routes
admin := s.server.Group("/admin") admin := s.server.Group("/admin")
admin.Use(s.RequireAuthentication(s.config.AdminLdapGroup)) admin.Use(s.RequireAuthentication("admin"))
admin.GET("/", s.GetAdminIndex) admin.GET("/", s.GetAdminIndex)
admin.GET("/device/edit", s.GetAdminEditInterface) admin.GET("/device/edit", s.GetAdminEditInterface)
admin.POST("/device/edit", s.PostAdminEditInterface) admin.POST("/device/edit", s.PostAdminEditInterface)
@ -34,6 +43,12 @@ func SetupRoutes(s *Server) {
admin.GET("/peer/download", s.GetPeerConfig) admin.GET("/peer/download", s.GetPeerConfig)
admin.GET("/peer/email", s.GetPeerConfigMail) admin.GET("/peer/email", s.GetPeerConfigMail)
admin.GET("/users/", s.GetAdminUsersIndex)
admin.GET("/users/create", s.GetAdminUsersCreate)
admin.POST("/users/create", s.PostAdminUsersCreate)
admin.GET("/users/edit", s.GetAdminUsersEdit)
admin.POST("/users/edit", s.PostAdminUsersEdit)
// User routes // User routes
user := s.server.Group("/user") user := s.server.Group("/user")
user.Use(s.RequireAuthentication("")) // empty scope = all logged in users user.Use(s.RequireAuthentication("")) // empty scope = all logged in users
@ -46,7 +61,7 @@ func SetupRoutes(s *Server) {
func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc { func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
session := s.getSessionData(c) session := GetSessionData(c)
if !session.LoggedIn { if !session.LoggedIn {
// Abort the request with the appropriate error code // Abort the request with the appropriate error code
@ -55,8 +70,15 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
return return
} }
if scope != "" && !session.IsAdmin && // admins always have access if scope == "admin" && !session.IsAdmin {
!s.ldapUsers.IsInGroup(session.UserName, scope) { // Abort the request with the appropriate error code
c.Abort()
s.GetHandleError(c, http.StatusUnauthorized, "unauthorized", "not enough permissions")
return
}
// default case if some randome scope was set...
if scope != "" && !session.IsAdmin {
// Abort the request with the appropriate error code // Abort the request with the appropriate error code
c.Abort() c.Abort()
s.GetHandleError(c, http.StatusUnauthorized, "unauthorized", "not enough permissions") s.GetHandleError(c, http.StatusUnauthorized, "unauthorized", "not enough permissions")

17
internal/users/config.go Normal file
View File

@ -0,0 +1,17 @@
package users
type SupportedDatabase string
const (
SupportedDatabaseMySQL SupportedDatabase = "mysql"
SupportedDatabaseSQLite SupportedDatabase = "sqlite"
)
type Config struct {
Typ SupportedDatabase `yaml:"typ" envconfig:"DATABASE_TYPE"` //mysql or sqlite
Host string `yaml:"host" envconfig:"DATABASE_HOST"`
Port int `yaml:"port" envconfig:"DATABASE_PORT"`
Database string `yaml:"database" envconfig:"DATABASE_NAME"` // On SQLite: the database file-path, otherwise the database name
User string `yaml:"user" envconfig:"DATABASE_USERNAME"`
Password string `yaml:"password" envconfig:"DATABASE_PASSWORD"`
}

263
internal/users/manager.go Normal file
View File

@ -0,0 +1,263 @@
package users
import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func GetDatabaseForConfig(cfg *Config) (db *gorm.DB, err error) {
switch cfg.Typ {
case SupportedDatabaseSQLite:
if _, err = os.Stat(filepath.Dir(cfg.Database)); os.IsNotExist(err) {
if err = os.MkdirAll(filepath.Dir(cfg.Database), 0700); err != nil {
return
}
}
db, err = gorm.Open(sqlite.Open(cfg.Database), &gorm.Config{})
if err != nil {
return
}
case SupportedDatabaseMySQL:
connectionString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
db, err = gorm.Open(mysql.Open(connectionString), &gorm.Config{})
if err != nil {
return
}
sqlDB, _ := db.DB()
sqlDB.SetConnMaxLifetime(time.Minute * 5)
sqlDB.SetMaxIdleConns(2)
sqlDB.SetMaxOpenConns(10)
err = sqlDB.Ping() // This DOES open a connection if necessary. This makes sure the database is accessible
if err != nil {
return nil, errors.Wrap(err, "failed to ping mysql authentication database")
}
}
// Enable Logger (logrus)
logCfg := logger.Config{
SlowThreshold: time.Second, // all slower than one second
Colorful: false,
LogLevel: logger.Silent, // default: log nothing
}
if logrus.StandardLogger().GetLevel() == logrus.TraceLevel {
logCfg.LogLevel = logger.Info
logCfg.SlowThreshold = 500 * time.Millisecond // all slower than half a second
}
db.Config.Logger = logger.New(logrus.StandardLogger(), logCfg)
return
}
type Manager struct {
db *gorm.DB
}
func NewManager(cfg *Config) (*Manager, error) {
m := &Manager{}
var err error
m.db, err = GetDatabaseForConfig(cfg)
if err != nil {
return nil, errors.Wrapf(err, "failed to setup user database %s", cfg.Database)
}
return m, m.MigrateUserDB()
}
func (m Manager) MigrateUserDB() error {
if err := m.db.AutoMigrate(&User{}); err != nil {
return errors.Wrap(err, "failed to migrate user database")
}
return nil
}
func (m Manager) GetUsers() []User {
users := make([]User, 0)
m.db.Find(&users)
return users
}
func (m Manager) GetUsersUnscoped() []User {
users := make([]User, 0)
m.db.Unscoped().Find(&users)
return users
}
func (m Manager) UserExists(email string) bool {
return m.GetUser(email) != nil
}
func (m Manager) GetUser(email string) *User {
user := User{}
m.db.Where("email = ?", email).First(&user)
if user.Email != email {
return nil
}
return &user
}
func (m Manager) GetUserUnscoped(email string) *User {
user := User{}
m.db.Unscoped().Where("email = ?", email).First(&user)
if user.Email != email {
return nil
}
return &user
}
func (m Manager) GetFilteredAndSortedUsers(sortKey, sortDirection, search string) []User {
users := make([]User, 0)
m.db.Find(&users)
filteredUsers := filterUsers(users, search)
sortUsers(filteredUsers, sortKey, sortDirection)
return filteredUsers
}
func (m Manager) GetFilteredAndSortedUsersUnscoped(sortKey, sortDirection, search string) []User {
users := make([]User, 0)
m.db.Unscoped().Find(&users)
filteredUsers := filterUsers(users, search)
sortUsers(filteredUsers, sortKey, sortDirection)
return filteredUsers
}
func (m Manager) GetOrCreateUser(email string) (*User, error) {
user := User{}
m.db.Where("email = ?", email).FirstOrInit(&user)
if user.Email != email {
user.Email = email
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
user.IsAdmin = false
user.Source = UserSourceDatabase
res := m.db.Create(&user)
if res.Error != nil {
return nil, errors.Wrapf(res.Error, "failed to create user %s", email)
}
}
return &user, nil
}
func (m Manager) GetOrCreateUserUnscoped(email string) (*User, error) {
user := User{}
m.db.Unscoped().Where("email = ?", email).FirstOrInit(&user)
if user.Email != email {
user.Email = email
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
user.IsAdmin = false
user.Source = UserSourceDatabase
res := m.db.Create(&user)
if res.Error != nil {
return nil, errors.Wrapf(res.Error, "failed to create user %s", email)
}
}
return &user, nil
}
func (m Manager) CreateUser(user *User) error {
res := m.db.Create(user)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to create user %s", user.Email)
}
return nil
}
func (m Manager) UpdateUser(user *User) error {
res := m.db.Save(user)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to update user %s", user.Email)
}
return nil
}
func (m Manager) DeleteUser(user *User) error {
res := m.db.Delete(user)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to update user %s", user.Email)
}
return nil
}
func sortUsers(users []User, key, direction string) {
sort.Slice(users, func(i, j int) bool {
var sortValueLeft string
var sortValueRight string
switch key {
case "email":
sortValueLeft = users[i].Email
sortValueRight = users[j].Email
case "firstname":
sortValueLeft = users[i].Firstname
sortValueRight = users[j].Firstname
case "lastname":
sortValueLeft = users[i].Lastname
sortValueRight = users[j].Lastname
case "phone":
sortValueLeft = users[i].Phone
sortValueRight = users[j].Phone
case "source":
sortValueLeft = string(users[i].Source)
sortValueRight = string(users[j].Source)
case "admin":
sortValueLeft = strconv.FormatBool(users[i].IsAdmin)
sortValueRight = strconv.FormatBool(users[j].IsAdmin)
}
if direction == "asc" {
return sortValueLeft < sortValueRight
} else {
return sortValueLeft > sortValueRight
}
})
}
func filterUsers(users []User, search string) []User {
if search == "" {
return users
}
filteredUsers := make([]User, 0, len(users))
for i := range users {
if strings.Contains(users[i].Email, search) ||
strings.Contains(users[i].Firstname, search) ||
strings.Contains(users[i].Lastname, search) ||
strings.Contains(string(users[i].Source), search) ||
strings.Contains(users[i].Phone, search) {
filteredUsers = append(filteredUsers, users[i])
}
}
return filteredUsers
}

36
internal/users/user.go Normal file
View File

@ -0,0 +1,36 @@
package users
import (
"time"
"gorm.io/gorm"
)
type UserSource string
const (
UserSourceLdap UserSource = "ldap" // LDAP / ActiveDirectory
UserSourceDatabase UserSource = "db" // sqlite / mysql database
UserSourceOIDC UserSource = "oidc" // open id connect, TODO: implement
)
// User is the user model that gets linked to peer entries, by default an empty usermodel with only the email address is created
type User struct {
// required fields
Email string `gorm:"primaryKey" form:"email" binding:"required,email"`
Source UserSource
IsAdmin bool
// optional fields
Firstname string `form:"firstname" binding:"required"`
Lastname string `form:"lastname" binding:"required"`
Phone string `form:"phone" binding:"omitempty"`
// optional, integrated password authentication
Password string `form:"password" binding:"omitempty"`
// database internal fields
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}