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)
######-
FROM golang:1.15 as builder
FROM golang:1.16 as builder
RUN mkdir /build
@ -29,7 +29,7 @@ FROM debian:buster
ENV TZ=Europe/Vienna
# 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 && \
apt-get install --no-install-recommends -y moreutils ca-certificates curl && \
rm -rf /var/cache/apt /var/lib/apt/lists/*; \

View File

@ -11,12 +11,10 @@ IMAGE=h44z/wg-portal
all: dep build
build: dep $(addsuffix -amd64,$(addprefix $(BUILDDIR)/,$(BINARIES)))
cp -r assets $(BUILDDIR)
cp scripts/wg-portal.service $(BUILDDIR)
cp scripts/wg-portal.env $(BUILDDIR)
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.env $(BUILDDIR)

View File

@ -10,7 +10,7 @@ use the following instructions:
### Building
This section describes how to build the WireGuard Portal code.
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

View File

@ -7,14 +7,13 @@
![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/)
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
interface. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
connections.
The configuration portal is designed to use LDAP (Active Directory) 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
will only be available in combination with LDAP.
The configuration portal currently supports using SQLite, MySQL as a user source for authentication and profile data.
It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
## Features
* Self-hosted and web based
@ -24,18 +23,19 @@ will only be available in combination with LDAP.
* Enable / Disable clients seamlessly
* Generation of `wgX.conf` after any modification
* IPv6 ready
* User authentication (LDAP and/or predefined admin account)
* User authentication (SQLite/MySQL and LDAP)
* Dockerized
* Responsive template
* One single binary
![Screenshot](screenshot.png)
## Setup
### 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'
services:
@ -56,19 +56,20 @@ services:
- WEBSITE_TITLE=WireGuard VPN
- COMPANY_NAME=Your Company Name
- MAIL_FROM=WireGuard VPN <noreply+wireguard@company.com>
- ADMIN_USER=admin # optional admin user
- ADMIN_USER=admin@domain.com
- ADMIN_PASS=supersecret
- ADMIN_LDAP_GROUP=CN=WireGuardAdmins,OU=Users,DC=COMPANY,DC=LOCAL
- EMAIL_HOST=10.10.10.10
- EMAIL_PORT=25
- LDAP_ENABLED=true
- LDAP_URL=ldap://srv-ad01.company.local:389
- LDAP_BASEDN=DC=COMPANY,DC=LOCAL
- LDAP_USER=ldap_wireguard@company.local
- 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.
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
For a standalone application, use the Makefile provided in the repository to build the application.
@ -80,7 +81,7 @@ make
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).
## 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*/
.jumbotron-home {
padding: 1rem 1rem;
}
@media (min-width: 576px) {
.jumbotron-home {
padding: 2rem 2rem;
}
}
@media (min-width: 1440px) {
.container, .container-lg, .container-md, .container-sm, .container-xl {
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({
autocomplete: {
source: [{{range $i, $u :=.Users}}{{$u.Mail}},{{end}}],
source: [{{range $i, $u :=.Users}}{{$u.Email}},{{end}}],
delay: 100
},
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 class="mt-4 row">
<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 class="col-sm-2 col-12 text-right">
{{if not .Static.LdapDisabled}}
<a href="/admin/peer/createldap" title="Add LDAP users" class="btn btn-primary"><i class="fa fa-fw fa-user-plus"></i></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>
<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/create" title="Manually add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>M</a>
</div>
</div>
<div class="mt-2 table-responsive">
@ -98,11 +96,11 @@
<thead>
<tr>
<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=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "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=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "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=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 "peers" "pubKey"}}"></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 "peers" "ip"}}"></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 -->
</tr>
</thead>
@ -144,15 +142,14 @@
<div class="tab-content" id="tabContent{{$p.UID}}">
<div id="t1{{$p.UID}}" class="tab-pane fade active show">
<h4>User details</h4>
{{if not $p.LdapUser}}
<p>No LDAP user-information available...</p>
{{if not $p.User}}
<p>No user information available...</p>
{{else}}
<ul>
<li>Firstname: {{$p.LdapUser.Firstname}}</li>
<li>Lastname: {{$p.LdapUser.Lastname}}</li>
<li>Phone: {{index $p.LdapUser.RawLdapData.Attributes "telephoneNumber"}}</li>
<li>Mail: {{$p.LdapUser.Mail}}</li>
<li>Department: {{index $p.LdapUser.RawLdapData.Attributes "department"}}</li>
<li>Firstname: {{$p.User.Firstname}}</li>
<li>Lastname: {{$p.User.Lastname}}</li>
<li>Phone: {{$p.User.Phone}}</li>
<li>Mail: {{$p.User.Email}}</li>
</ul>
{{end}}
<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">
{{template "prt_nav.html" .}}
<div class="container mt-5">
<div class="container mt-2">
<div class="page-header">
<h1>WireGuard VPN Portal</h1>
</div>
{{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>
<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>
<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>
<div class="jumbotron jumbotron-home">
<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>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>

View File

@ -7,19 +7,28 @@
<div id="topNavbar" class="navbar-collapse collapse">
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
<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">
<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>
</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>
{{if eq $.Session.LoggedIn true}}
<div class="nav-item dropdown">
<a href="#" class="navbar-text dropdown-toggle" data-toggle="dropdown">{{$.Session.Firstname}} {{$.Session.Lastname}} <span class="caret"></span></a>
<div class="dropdown-menu">
{{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>
{{end}}{{end}}
<a class="dropdown-item" href="/user/profile"><i class="fas fa-user"></i> Profile</a>

View File

@ -21,11 +21,11 @@
<thead>
<tr>
<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=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "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=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "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=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 "userpeers" "pubKey"}}"></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 "userpeers" "ip"}}"></i></a></th>
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "handshake"}}"></i></a></th>
</tr>
</thead>
<tbody>
@ -58,15 +58,14 @@
<div class="tab-content" id="tabContent{{$p.UID}}">
<div id="t1{{$p.UID}}" class="tab-pane fade active show">
<h4>User details</h4>
{{if not $p.LdapUser}}
<p>No LDAP user-information available...</p>
{{if not $p.User}}
<p>No user information available...</p>
{{else}}
<ul>
<li>Firstname: {{$p.LdapUser.Firstname}}</li>
<li>Lastname: {{$p.LdapUser.Lastname}}</li>
<li>Phone: {{$p.UID}}</li>
<li>Mail: {{$p.LdapUser.Mail}}</li>
<li>Department: {{$p.UID}}</li>
<li>Firstname: {{$p.User.Firstname}}</li>
<li>Lastname: {{$p.User.Lastname}}</li>
<li>Phone: {{$p.User.Phone}}</li>
<li>Mail: {{$p.User.Email}}</li>
</ul>
{{end}}
<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
go 1.14
go 1.16
require (
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/kelseyhightower/envconfig v1.4.0
github.com/milosgajdos/tenus v0.0.3
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
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
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/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"
"github.com/h44z/wg-portal/internal/ldap"
"github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard"
"github.com/kelseyhightower/envconfig"
"github.com/sirupsen/logrus"
@ -54,22 +55,21 @@ func loadConfigEnv(cfg interface{}) error {
type Config struct {
Core struct {
ListeningAddress string `yaml:"listeningAddress" envconfig:"LISTENING_ADDRESS"`
ExternalUrl string `yaml:"externalUrl" envconfig:"EXTERNAL_URL"`
Title string `yaml:"title" envconfig:"WEBSITE_TITLE"`
CompanyName string `yaml:"company" envconfig:"COMPANY_NAME"`
MailFrom string `yaml:"mailfrom" envconfig:"MAIL_FROM"`
AdminUser string `yaml:"adminUser" envconfig:"ADMIN_USER"` // optional, non LDAP admin user
AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"`
DatabasePath string `yaml:"database" envconfig:"DATABASE_PATH"`
EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"`
CreateInterfaceOnLogin bool `yaml:"createOnLogin" envconfig:"CREATE_INTERFACE_ON_LOGIN"`
SyncLdapStatus bool `yaml:"syncLdapStatus" envconfig:"SYNC_LDAP_STATUS"` // disable account if disabled in ldap
ListeningAddress string `yaml:"listeningAddress" envconfig:"LISTENING_ADDRESS"`
ExternalUrl string `yaml:"externalUrl" envconfig:"EXTERNAL_URL"`
Title string `yaml:"title" envconfig:"WEBSITE_TITLE"`
CompanyName string `yaml:"company" envconfig:"COMPANY_NAME"`
MailFrom string `yaml:"mailFrom" envconfig:"MAIL_FROM"`
AdminUser string `yaml:"adminUser" envconfig:"ADMIN_USER"`
AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"`
EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"`
CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"`
LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
} `yaml:"core"`
Email MailConfig `yaml:"email"`
LDAP ldap.Config `yaml:"ldap"`
WG wireguard.Config `yaml:"wg"`
AdminLdapGroup string `yaml:"adminLdapGroup" envconfig:"ADMIN_LDAP_GROUP"`
Database users.Config `yaml:"database"`
Email MailConfig `yaml:"email"`
LDAP ldap.Config `yaml:"ldap"`
WG wireguard.Config `yaml:"wg"`
}
func NewConfig() *Config {
@ -81,18 +81,31 @@ func NewConfig() *Config {
cfg.Core.CompanyName = "WireGuard Portal"
cfg.Core.ExternalUrl = "http://localhost:8123"
cfg.Core.MailFrom = "WireGuard VPN <noreply@company.com>"
cfg.Core.AdminUser = "" // non-ldap admin access is disabled by default
cfg.Core.AdminPassword = ""
cfg.Core.DatabasePath = "data/wg_portal.db"
cfg.Core.AdminUser = "admin@wgportal.local"
cfg.Core.AdminPassword = "wgportal"
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.BaseDN = "DC=COMPANY,DC=LOCAL"
cfg.LDAP.StartTLS = true
cfg.LDAP.BindUser = "company\\\\ldap_wireguard"
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.WireGuardConfig = "/etc/wireguard/wg0.conf"
cfg.WG.ManageIPAddresses = true
cfg.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL"
cfg.Email.Host = "127.0.0.1"
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
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"`
import (
"crypto/tls"
"fmt"
"strconv"
"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 (
"context"
"encoding/gob"
"errors"
"html/template"
"io/fs"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"os"
"path/filepath"
@ -15,15 +16,18 @@ import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"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/ldap"
"github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
ginlogrus "github.com/toorop/gin-logrus"
)
const SessionIdentifier = "wgPortalSession"
const CacheRefreshDuration = 5 * time.Minute
func init() {
gob.Register(SessionData{})
@ -31,22 +35,23 @@ func init() {
gob.Register(Peer{})
gob.Register(Device{})
gob.Register(LdapCreateForm{})
gob.Register(users.User{})
}
type SessionData struct {
LoggedIn bool
IsAdmin bool
UID string
UserName string
Firstname string
Lastname string
Email string
SortedBy string
SortDirection string
Search string
AlertData string
AlertType string
FormData interface{}
LoggedIn bool
IsAdmin bool
Firstname string
Lastname string
Email string
SortedBy map[string]string
SortDirection map[string]string
Search map[string]string
AlertData string
AlertType string
FormData interface{}
}
type FlashData struct {
@ -60,28 +65,23 @@ type StaticData struct {
WebsiteLogo string
CompanyName string
Year int
LdapDisabled bool
}
type Server struct {
// Core components
ctx context.Context
config *common.Config
server *gin.Engine
users *UserManager
mailTpl *template.Template
auth *AuthManager
// WireGuard stuff
wg *wireguard.Manager
// LDAP stuff
ldapDisabled bool
ldapAuth ldap.Authentication
ldapUsers *ldap.SynchronizedUserCacheHolder
ldapCacheUpdater *ldap.UserCache
users *users.Manager
wg *wireguard.Manager
peers *PeerManager
}
func (s *Server) Setup(ctx context.Context) error {
var err error
dir := s.getExecutableDirectory()
rDir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
logrus.Infof("Real working directory: %s", rDir)
@ -91,44 +91,10 @@ func (s *Server) Setup(ctx context.Context) error {
rand.Seed(time.Now().UnixNano())
s.config = common.NewConfig()
// 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")
}
s.ctx = ctx
// Setup http server
gin.SetMode(gin.ReleaseMode)
gin.SetMode(gin.DebugMode)
gin.DefaultWriter = ioutil.Discard
s.server = gin.New()
s.server.Use(ginlogrus.Logger(logrus.StandardLogger()), gin.Recovery())
@ -138,54 +104,98 @@ func (s *Server) Setup(ctx context.Context) error {
})
// Setup templates
logrus.Infof("Loading templates from: %s", filepath.Join(dir, "/assets/tpl/*.html"))
s.server.LoadHTMLGlob(filepath.Join(dir, "/assets/tpl/*.html"))
templates := template.Must(template.New("").Funcs(s.server.FuncMap).ParseFS(wg_portal.Templates, "assets/tpl/*.html"))
s.server.SetHTMLTemplate(templates)
s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte("secret")))) // TODO: change key?
// Serve static files
s.server.Static("/css", filepath.Join(dir, "/assets/css"))
s.server.Static("/js", filepath.Join(dir, "/assets/js"))
s.server.Static("/img", filepath.Join(dir, "/assets/img"))
s.server.Static("/fonts", filepath.Join(dir, "/assets/fonts"))
s.server.StaticFS("/css", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/css"))))
s.server.StaticFS("/js", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/js"))))
s.server.StaticFS("/img", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/img"))))
s.server.StaticFS("/fonts", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/fonts"))))
// Setup all routes
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!")
return nil
}
func (s *Server) Run() {
// Start ldap group watcher
if !s.ldapDisabled {
go func(s *Server) {
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)
// Start ldap sync
if s.config.Core.LdapEnabled {
go s.SyncLdapWithUserDatabase()
}
// Run web service
err := s.server.Run(s.config.Core.ListeningAddress)
if err != nil {
logrus.Errorf("Failed to listen and serve on %s: %v", s.config.Core.ListeningAddress, err)
srv := &http.Server{
Addr: s.config.Core.ListeningAddress,
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 {
@ -201,7 +211,16 @@ func (s *Server) getExecutableDirectory() string {
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)
rawSessionData := session.Get(SessionIdentifier)
@ -210,8 +229,9 @@ func (s *Server) getSessionData(c *gin.Context) SessionData {
sessionData = rawSessionData.(SessionData)
} else {
sessionData = SessionData{
SortedBy: "mail",
SortDirection: "asc",
Search: map[string]string{"peers": "", "userpeers": "", "users": ""},
SortedBy: map[string]string{"peers": "mail", "userpeers": "mail", "users": "email"},
SortDirection: map[string]string{"peers": "asc", "userpeers": "asc", "users": "asc"},
Email: "",
Firstname: "",
Lastname: "",
@ -227,7 +247,7 @@ func (s *Server) getSessionData(c *gin.Context) SessionData {
return sessionData
}
func (s *Server) getFlashes(c *gin.Context) []FlashData {
func GetFlashes(c *gin.Context) []FlashData {
session := sessions.Default(c)
flashes := session.Flashes()
if err := session.Save(); err != nil {
@ -242,7 +262,7 @@ func (s *Server) getFlashes(c *gin.Context) []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.Set(SessionIdentifier, data)
if err := session.Save(); err != nil {
@ -252,7 +272,7 @@ func (s *Server) updateSessionData(c *gin.Context, data SessionData) error {
return nil
}
func (s *Server) destroySessionData(c *gin.Context) error {
func DestroySessionData(c *gin.Context) error {
session := sessions.Default(c)
session.Delete(SessionIdentifier)
if err := session.Save(); err != nil {
@ -262,17 +282,7 @@ func (s *Server) destroySessionData(c *gin.Context) error {
return nil
}
func (s *Server) getStaticData() StaticData {
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) {
func SetFlashMessage(c *gin.Context, message, typ string) {
session := sessions.Default(c)
session.AddFlash(FlashData{
Message: message,
@ -283,13 +293,20 @@ func (s *Server) setFlashMessage(c *gin.Context, message, typ string) {
}
}
func (s SessionData) GetSortIcon(field string) string {
if s.SortedBy != field {
func (s SessionData) GetSortIcon(table, field string) string {
if s.SortedBy[table] != field {
return "fa-sort"
}
if s.SortDirection == "asc" {
if s.SortDirection[table] == "asc" {
return "fa-sort-alpha-down"
} else {
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"
"github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/authentication"
"github.com/h44z/wg-portal/internal/users"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func (s *Server) GetLogin(c *gin.Context) {
currentSession := s.getSessionData(c)
currentSession := GetSessionData(c)
if currentSession.LoggedIn {
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) {
currentSession := s.getSessionData(c)
currentSession := GetSessionData(c)
if currentSession.LoggedIn {
// already logged in
c.Redirect(http.StatusSeeOther, "/")
@ -49,59 +52,84 @@ func (s *Server) PostLogin(c *gin.Context) {
return
}
adminAuthenticated := false
if s.config.Core.AdminUser != "" && username == s.config.Core.AdminUser && password == s.config.Core.AdminPassword {
adminAuthenticated = true
// Check user database for an matching entry
var loginProvider authentication.AuthProvider
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
}
email = authEmail
loginProvider = provider
// 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 user is in cache, avoid unnecessary ldap requests
if !adminAuthenticated && !s.ldapUsers.UserExists(username) {
c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail")
}
// Check if username and password match
if !adminAuthenticated && !s.ldapAuth.CheckLogin(username, password) {
// Check if user is authenticated
if email == "" || loginProvider == nil {
c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail")
return
}
var sessionData SessionData
if adminAuthenticated {
sessionData = SessionData{
LoggedIn: true,
IsAdmin: true,
Email: "autodetected@example.com",
UID: "adminuid",
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: "",
}
}
// Set authenticated session
sessionData := GetSessionData(c)
sessionData.LoggedIn = true
sessionData.IsAdmin = user.IsAdmin
sessionData.Email = user.Email
sessionData.Firstname = user.Firstname
sessionData.Lastname = user.Lastname
// Check if user already has a peer setup, if not create one
if s.config.Core.CreateInterfaceOnLogin && !adminAuthenticated {
users := s.users.GetUsersByMail(sessionData.Email)
if len(users) == 0 { // Create vpn peer
err := s.CreateUser(Peer{
if s.config.Core.CreateDefaultPeer {
peers := s.peers.GetPeersByMail(sessionData.Email)
if len(peers) == 0 { // Create vpn peer
err := s.CreatePeer(Peer{
Identifier: sessionData.Firstname + " " + sessionData.Lastname + " (Default)",
Email: 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")
return
}
@ -119,14 +147,14 @@ func (s *Server) PostLogin(c *gin.Context) {
}
func (s *Server) GetLogout(c *gin.Context) {
currentSession := s.getSessionData(c)
currentSession := GetSessionData(c)
if !currentSession.LoggedIn { // Not logged in
c.Redirect(http.StatusSeeOther, "/")
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")
return
}

View File

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

View File

@ -9,8 +9,8 @@ import (
)
func (s *Server) GetAdminEditInterface(c *gin.Context) {
device := s.users.GetDevice()
users := s.users.GetAllUsers()
device := s.peers.GetDevice()
users := s.peers.GetAllPeers()
currentSession, err := s.setFormInSession(c, device)
if err != nil {
@ -28,7 +28,7 @@ func (s *Server) GetAdminEditInterface(c *gin.Context) {
EditableKeys bool
}{
Route: c.Request.URL.Path,
Alerts: s.getFlashes(c),
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Peers: users,
@ -38,14 +38,14 @@ func (s *Server) GetAdminEditInterface(c *gin.Context) {
}
func (s *Server) PostAdminEditInterface(c *gin.Context) {
currentSession := s.getSessionData(c)
currentSession := GetSessionData(c)
var formDevice Device
if currentSession.FormData != nil {
formDevice = currentSession.FormData.(Device)
}
if err := c.ShouldBind(&formDevice); err != nil {
_ = s.updateFormInSession(c, formDevice)
s.setFlashMessage(c, err.Error(), "danger")
SetFlashMessage(c, err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=bind")
return
}
@ -61,16 +61,16 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetConfig())
if err != nil {
_ = 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")
return
}
// Update in database
err = s.users.UpdateDevice(formDevice)
err = s.peers.UpdateDevice(formDevice)
if err != nil {
_ = 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")
return
}
@ -79,7 +79,7 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
err = s.WriteWireGuardConfigFile()
if err != nil {
_ = 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")
return
}
@ -88,26 +88,26 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
if s.config.WG.ManageIPAddresses {
if err := s.wg.SetIPAddress(formDevice.IPs); err != nil {
_ = 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")
}
if err := s.wg.SetMTU(formDevice.Mtu); err != nil {
_ = 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")
}
}
s.setFlashMessage(c, "Changes applied successfully!", "success")
SetFlashMessage(c, "Changes applied successfully!", "success")
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")
}
func (s *Server) GetInterfaceConfig(c *gin.Context) {
device := s.users.GetDevice()
users := s.users.GetActiveUsers()
device := s.peers.GetDevice()
users := s.peers.GetActivePeers()
cfg, err := device.GetConfigFile(users)
if err != nil {
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) {
device := s.users.GetDevice()
users := s.users.GetAllUsers()
device := s.peers.GetDevice()
users := s.peers.GetAllPeers()
for _, user := range users {
user.AllowedIPs = device.AllowedIPs
user.AllowedIPsStr = device.AllowedIPsStr
if err := s.users.UpdateUser(user); err != nil {
s.setFlashMessage(c, err.Error(), "danger")
if err := s.peers.UpdatePeer(user); err != nil {
SetFlashMessage(c, err.Error(), "danger")
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")
return
}

View File

@ -10,7 +10,7 @@ import (
"github.com/gin-gonic/gin"
"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/tatsushid/go-fastping"
)
@ -21,10 +21,10 @@ type LdapCreateForm struct {
}
func (s *Server) GetAdminEditPeer(c *gin.Context) {
device := s.users.GetDevice()
user := s.users.GetUserByKey(c.Query("pkey"))
device := s.peers.GetDevice()
peer := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession, err := s.setFormInSession(c, user)
currentSession, err := s.setFormInSession(c, peer)
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
return
@ -40,7 +40,7 @@ func (s *Server) GetAdminEditPeer(c *gin.Context) {
EditableKeys bool
}{
Route: c.Request.URL.Path,
Alerts: s.getFlashes(c),
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Peer: currentSession.FormData.(Peer),
@ -50,17 +50,17 @@ func (s *Server) GetAdminEditPeer(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"))
currentSession := s.getSessionData(c)
currentSession := GetSessionData(c)
var formPeer Peer
if currentSession.FormData != nil {
formPeer = currentSession.FormData.(Peer)
}
if err := c.ShouldBind(&formPeer); err != nil {
_ = 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")
return
}
@ -73,28 +73,28 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) {
disabled := c.PostForm("isdisabled") != ""
now := time.Now()
if disabled && currentUser.DeactivatedAt == nil {
if disabled && currentPeer.DeactivatedAt == nil {
formPeer.DeactivatedAt = &now
} else if !disabled {
formPeer.DeactivatedAt = nil
}
// Update in database
if err := s.UpdateUser(formPeer, now); err != nil {
if err := s.UpdatePeer(formPeer, now); err != nil {
_ = 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")
return
}
s.setFlashMessage(c, "changes applied successfully", "success")
SetFlashMessage(c, "changes applied successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey)
}
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 {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
return
@ -109,7 +109,7 @@ func (s *Server) GetAdminCreatePeer(c *gin.Context) {
EditableKeys bool
}{
Route: c.Request.URL.Path,
Alerts: s.getFlashes(c),
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Peer: currentSession.FormData.(Peer),
@ -119,14 +119,14 @@ func (s *Server) GetAdminCreatePeer(c *gin.Context) {
}
func (s *Server) PostAdminCreatePeer(c *gin.Context) {
currentSession := s.getSessionData(c)
currentSession := GetSessionData(c)
var formPeer Peer
if currentSession.FormData != nil {
formPeer = currentSession.FormData.(Peer)
}
if err := c.ShouldBind(&formPeer); err != nil {
_ = 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")
return
}
@ -143,14 +143,14 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) {
formPeer.DeactivatedAt = &now
}
if err := s.CreateUser(formPeer); err != nil {
if err := s.CreatePeer(formPeer); err != nil {
_ = 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")
return
}
s.setFlashMessage(c, "client created successfully", "success")
SetFlashMessage(c, "client created successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin")
}
@ -166,29 +166,29 @@ func (s *Server) GetAdminCreateLdapPeers(c *gin.Context) {
Alerts []FlashData
Session SessionData
Static StaticData
Users []*ldap.UserCacheHolderEntry
Users []users.User
FormData LdapCreateForm
Device Device
}{
Route: c.Request.URL.Path,
Alerts: s.getFlashes(c),
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Users: s.ldapUsers.GetSortedUsers("sn", "asc"),
Users: s.users.GetFilteredAndSortedUsers("lastname", "asc", ""),
FormData: currentSession.FormData.(LdapCreateForm),
Device: s.users.GetDevice(),
Device: s.peers.GetDevice(),
})
}
func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
currentSession := s.getSessionData(c)
currentSession := GetSessionData(c)
var formData LdapCreateForm
if currentSession.FormData != nil {
formData = currentSession.FormData.(LdapCreateForm)
}
if err := c.ShouldBind(&formData); err != nil {
_ = 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")
return
}
@ -196,9 +196,9 @@ func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
emails := common.ParseStringList(formData.Emails)
for i := range emails {
// 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.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")
return
}
@ -207,31 +207,31 @@ func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
logrus.Infof("creating %d ldap peers", len(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.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")
return
}
}
s.setFlashMessage(c, "client(s) created successfully", "success")
SetFlashMessage(c, "client(s) created successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin/peer/createldap")
}
func (s *Server) GetAdminDeletePeer(c *gin.Context) {
currentUser := s.users.GetUserByKey(c.Query("pkey"))
if err := s.DeleteUser(currentUser); err != nil {
currentUser := s.peers.GetPeerByKey(c.Query("pkey"))
if err := s.DeletePeer(currentUser); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Deletion error", err.Error())
return
}
s.setFlashMessage(c, "user deleted successfully", "success")
SetFlashMessage(c, "user deleted successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin")
}
func (s *Server) GetPeerQRCode(c *gin.Context) {
user := s.users.GetUserByKey(c.Query("pkey"))
currentSession := s.getSessionData(c)
user := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return
@ -247,14 +247,14 @@ func (s *Server) GetPeerQRCode(c *gin.Context) {
}
func (s *Server) GetPeerConfig(c *gin.Context) {
user := s.users.GetUserByKey(c.Query("pkey"))
currentSession := s.getSessionData(c)
user := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return
}
cfg, err := user.GetConfigFile(s.users.GetDevice())
cfg, err := user.GetConfigFile(s.peers.GetDevice())
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return
@ -266,14 +266,14 @@ func (s *Server) GetPeerConfig(c *gin.Context) {
}
func (s *Server) GetPeerConfigMail(c *gin.Context) {
user := s.users.GetUserByKey(c.Query("pkey"))
currentSession := s.getSessionData(c)
user := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return
}
cfg, err := user.GetConfigFile(s.users.GetDevice())
cfg, err := user.GetConfigFile(s.peers.GetDevice())
if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return
@ -319,13 +319,13 @@ func (s *Server) GetPeerConfigMail(c *gin.Context) {
return
}
s.setFlashMessage(c, "mail sent successfully", "success")
SetFlashMessage(c, "mail sent successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin")
}
func (s *Server) GetPeerStatus(c *gin.Context) {
user := s.users.GetUserByKey(c.Query("pkey"))
currentSession := s.getSessionData(c)
user := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
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 (
"crypto/md5"
"errors"
"fmt"
"io/ioutil"
"syscall"
"time"
"github.com/h44z/wg-portal/internal/common"
"github.com/pkg/errors"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func (s *Server) PrepareNewUser() (Peer, error) {
device := s.users.GetDevice()
func (s *Server) PrepareNewPeer() (Peer, error) {
device := s.peers.GetDevice()
peer := Peer{}
peer.IsNew = true
peer.AllowedIPsStr = device.AllowedIPsStr
peer.IPs = make([]string, len(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 {
return Peer{}, err
}
@ -43,18 +43,19 @@ func (s *Server) PrepareNewUser() (Peer, error) {
return peer, nil
}
func (s *Server) CreateUserByEmail(email, identifierSuffix string, disabled bool) error {
ldapUser := s.ldapUsers.GetUserData(s.ldapUsers.GetUserDNByMail(email))
if ldapUser.DN == "" {
return errors.New("no peer with email " + email + " found")
func (s *Server) CreatePeerByEmail(email, identifierSuffix string, disabled bool) error {
user, err := s.users.GetOrCreateUser(email)
if err != nil {
return errors.WithMessagef(err, "failed to load/create related user %s", email)
}
device := s.users.GetDevice()
device := s.peers.GetDevice()
peer := Peer{}
peer.User = user
peer.AllowedIPsStr = device.AllowedIPsStr
peer.IPs = make([]string, len(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 {
return err
}
@ -74,30 +75,30 @@ func (s *Server) CreateUserByEmail(email, identifierSuffix string, disabled bool
peer.PublicKey = key.PublicKey().String()
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
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()
if disabled {
peer.DeactivatedAt = &now
}
return s.CreateUser(peer)
return s.CreatePeer(peer)
}
func (s *Server) CreateUser(user Peer) error {
device := s.users.GetDevice()
user.AllowedIPsStr = device.AllowedIPsStr
if len(user.IPs) == 0 {
func (s *Server) CreatePeer(peer Peer) error {
device := s.peers.GetDevice()
peer.AllowedIPsStr = device.AllowedIPsStr
if peer.IPs == nil || len(peer.IPs) == 0 {
peer.IPs = make([]string, len(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 {
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()
if err != nil {
return err
@ -106,60 +107,60 @@ func (s *Server) CreateUser(user Peer) error {
if err != nil {
return err
}
user.PresharedKey = psk.String()
user.PrivateKey = key.String()
user.PublicKey = key.PublicKey().String()
peer.PresharedKey = psk.String()
peer.PrivateKey = key.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
if user.DeactivatedAt == nil {
if err := s.wg.AddPeer(user.GetConfig()); err != nil {
if peer.DeactivatedAt == nil {
if err := s.wg.AddPeer(peer.GetConfig()); err != nil {
return err
}
}
// Create in database
if err := s.users.CreateUser(user); err != nil {
if err := s.peers.CreatePeer(peer); err != nil {
return err
}
return s.WriteWireGuardConfigFile()
}
func (s *Server) UpdateUser(user Peer, updateTime time.Time) error {
currentUser := s.users.GetUserByKey(user.PublicKey)
func (s *Server) UpdatePeer(peer Peer, updateTime time.Time) error {
currentPeer := s.peers.GetPeerByKey(peer.PublicKey)
// Update WireGuard device
var err error
switch {
case user.DeactivatedAt == &updateTime:
err = s.wg.RemovePeer(user.PublicKey)
case user.DeactivatedAt == nil && currentUser.Peer != nil:
err = s.wg.UpdatePeer(user.GetConfig())
case user.DeactivatedAt == nil && currentUser.Peer == nil:
err = s.wg.AddPeer(user.GetConfig())
case peer.DeactivatedAt == &updateTime:
err = s.wg.RemovePeer(peer.PublicKey)
case peer.DeactivatedAt == nil && currentPeer.Peer != nil:
err = s.wg.UpdatePeer(peer.GetConfig())
case peer.DeactivatedAt == nil && currentPeer.Peer == nil:
err = s.wg.AddPeer(peer.GetConfig())
}
if err != nil {
return err
}
// Update in database
if err := s.users.UpdateUser(user); err != nil {
if err := s.peers.UpdatePeer(peer); err != nil {
return err
}
return s.WriteWireGuardConfigFile()
}
func (s *Server) DeleteUser(user Peer) error {
func (s *Server) DeletePeer(peer Peer) error {
// Delete WireGuard peer
if err := s.wg.RemovePeer(user.PublicKey); err != nil {
if err := s.wg.RemovePeer(peer.PublicKey); err != nil {
return err
}
// Delete in database
if err := s.users.DeleteUser(user); err != nil {
if err := s.peers.DeletePeer(peer); err != nil {
return err
}
@ -167,11 +168,11 @@ func (s *Server) DeleteUser(user Peer) error {
}
func (s *Server) RestoreWireGuardInterface() error {
activeUsers := s.users.GetActiveUsers()
activePeers := s.peers.GetActivePeers()
for i := range activeUsers {
if activeUsers[i].Peer == nil {
if err := s.wg.AddPeer(activeUsers[i].GetConfig()); err != nil {
for i := range activePeers {
if activePeers[i].Peer == nil {
if err := s.wg.AddPeer(activePeers[i].GetConfig()); err != nil {
return err
}
}
@ -188,8 +189,8 @@ func (s *Server) WriteWireGuardConfigFile() error {
return err
}
device := s.users.GetDevice()
cfg, err := device.GetConfigFile(s.users.GetActiveUsers())
device := s.peers.GetDevice()
cfg, err := device.GetConfigFile(s.peers.GetActivePeers())
if err != nil {
return err
}

View File

@ -4,31 +4,147 @@ import (
"time"
"github.com/h44z/wg-portal/internal/ldap"
"github.com/h44z/wg-portal/internal/users"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// SyncLdapAttributesWithWireGuard starts to synchronize the "disabled" attribute from ldap.
// Users will be automatically disabled once they are disabled in ldap.
// This method is blocking.
func (s *Server) SyncLdapAttributesWithWireGuard() error {
allUsers := s.users.GetAllUsers()
for i := range allUsers {
user := allUsers[i]
if user.LdapUser == nil {
continue // skip non ldap users
func (s *Server) SyncLdapWithUserDatabase() {
logrus.Info("starting ldap user synchronization...")
running := true
for running {
// Select blocks until one of the cases happens
select {
case <-time.After(1 * time.Minute):
// Sleep for 1 minute
case <-s.ctx.Done():
logrus.Trace("ldap-sync shutting down (context ended)...")
running = false
continue
}
if user.DeactivatedAt != nil {
continue // skip already disabled interfaces
// Main work here
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"]) {
now := time.Now()
user.DeactivatedAt = &now
if err := s.UpdateUser(user, now); err != nil {
logrus.Errorf("Failed to disable user %s: %v", user.Email, err)
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()
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(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)
}
}
}
}
// 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
}
}
}
}
}
return nil
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 (
"bytes"
"crypto/md5"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
@ -18,12 +15,12 @@ import (
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"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/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/skip2/go-qrcode"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
@ -68,9 +65,9 @@ func init() {
//
type Peer struct {
Peer *wgtypes.Peer `gorm:"-"`
LdapUser *ldap.UserCacheHolderEntry `gorm:"-"` // optional, it is still possible to have users without ldap
Config string `gorm:"-"`
Peer *wgtypes.Peer `gorm:"-"` // WireGuard peer
User *users.User `gorm:"-"` // user reference for the peer
Config string `gorm:"-"`
UID string `form:"uid" binding:"alphanum"` // uid for html identification
IsOnline bool `gorm:"-"`
@ -247,7 +244,7 @@ func (d Device) GetConfig() wgtypes.Config {
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)
if err != nil {
return nil, err
@ -259,7 +256,7 @@ func (d Device) GetConfigFile(clients []Peer) ([]byte, error) {
Clients []Peer
Server Device
}{
Clients: clients,
Clients: peers,
Server: d,
})
if err != nil {
@ -270,78 +267,65 @@ func (d Device) GetConfigFile(clients []Peer) ([]byte, error) {
}
//
// USER-MANAGER --------------------------------------------------------------------------------
// PEER-MANAGER --------------------------------------------------------------------------------
//
type UserManager struct {
db *gorm.DB
wg *wireguard.Manager
ldapUsers *ldap.SynchronizedUserCacheHolder
type PeerManager struct {
db *gorm.DB
wg *wireguard.Manager
users *users.Manager
}
func NewUserManager(dbPath string, wg *wireguard.Manager, ldapUsers *ldap.SynchronizedUserCacheHolder) *UserManager {
um := &UserManager{wg: wg, ldapUsers: ldapUsers}
func NewPeerManager(cfg *common.Config, wg *wireguard.Manager, userDB *users.Manager) (*PeerManager, error) {
um := &PeerManager{wg: wg, users: userDB}
var err error
if _, err = os.Stat(filepath.Dir(dbPath)); os.IsNotExist(err) {
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{})
um.db, err = users.GetDatabaseForConfig(&cfg.Database)
if err != nil {
logrus.Errorf("failed to open sqlite database (%s): %v", dbPath, err)
return nil
return nil, errors.WithMessage(err, "failed to open peer database")
}
err = um.db.AutoMigrate(&Peer{}, &Device{})
if err != nil {
logrus.Errorf("failed to migrate sqlite database: %v", err)
return nil
return nil, errors.WithMessage(err, "failed to migrate peer database")
}
return um
return um, nil
}
func (u *UserManager) InitFromCurrentInterface() error {
func (u *PeerManager) InitFromCurrentInterface() error {
peers, err := u.wg.GetPeerList()
if err != nil {
logrus.Errorf("failed to init user-manager from peers: %v", err)
return err
return errors.Wrapf(err, "failed to get peer list")
}
device, err := u.wg.GetDeviceInfo()
if err != nil {
logrus.Errorf("failed to init user-manager from device: %v", err)
return err
return errors.Wrapf(err, "failed to get device info")
}
var ipAddresses []string
var mtu int
if u.wg.Cfg.ManageIPAddresses {
if ipAddresses, err = u.wg.GetIPAddress(); err != nil {
logrus.Errorf("failed to init user-manager from device: %v", err)
return err
return errors.Wrapf(err, "failed to get ip address")
}
if mtu, err = u.wg.GetMTU(); err != nil {
logrus.Errorf("failed to init user-manager from device: %v", err)
return err
return errors.Wrapf(err, "failed to get MTU")
}
}
// Check if entries already exist in database, if not create them
for _, peer := range peers {
if err := u.validateOrCreateUserForPeer(peer); err != nil {
return err
if err := u.validateOrCreatePeer(peer); err != nil {
return errors.WithMessagef(err, "failed to validate peer %s", peer.PublicKey)
}
}
if err := u.validateOrCreateDevice(*device, ipAddresses, mtu); err != nil {
return err
return errors.WithMessagef(err, "failed to validate device %s", device.Name)
}
return nil
}
func (u *UserManager) validateOrCreateUserForPeer(wgPeer wgtypes.Peer) error {
func (u *PeerManager) validateOrCreatePeer(wgPeer wgtypes.Peer) error {
peer := 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)
if res.Error != nil {
logrus.Errorf("failed to create autodetected wgPeer: %v", res.Error)
return res.Error
return errors.Wrapf(res.Error, "failed to create autodetected peer %s", peer.PublicKey)
}
}
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{}
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)
if res.Error != nil {
logrus.Errorf("failed to create autodetected device: %v", res.Error)
return res.Error
return errors.Wrapf(res.Error, "failed to create autodetected device")
}
}
return nil
}
func (u *UserManager) populateUserData(user *Peer) {
user.AllowedIPs = strings.Split(user.AllowedIPsStr, ", ")
user.IPs = strings.Split(user.IPsStr, ", ")
func (u *PeerManager) populatePeerData(peer *Peer) {
peer.AllowedIPs = strings.Split(peer.AllowedIPsStr, ", ")
peer.IPs = strings.Split(peer.IPsStr, ", ")
// Set config file
tmpCfg, _ := user.GetConfigFile(u.GetDevice())
user.Config = string(tmpCfg)
tmpCfg, _ := peer.GetConfigFile(u.GetDevice())
peer.Config = string(tmpCfg)
// set data from WireGuard interface
user.Peer, _ = u.wg.GetPeer(user.PublicKey)
user.LastHandshake = "never"
user.LastHandshakeTime = "Never connected, or user is disabled."
if user.Peer != nil {
since := time.Since(user.Peer.LastHandshakeTime)
peer.Peer, _ = u.wg.GetPeer(peer.PublicKey)
peer.LastHandshake = "never"
peer.LastHandshakeTime = "Never connected, or user is disabled."
if peer.Peer != nil {
since := time.Since(peer.Peer.LastHandshakeTime)
sinceSeconds := int(since.Round(time.Second).Seconds())
sinceMinutes := int(sinceSeconds / 60)
sinceSeconds -= sinceMinutes * 60
if sinceMinutes > 2*10080 { // 2 weeks
user.LastHandshake = "a while ago"
peer.LastHandshake = "a while ago"
} else if sinceMinutes > 10080 { // 1 week
user.LastHandshake = "a week ago"
peer.LastHandshake = "a week ago"
} 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
user.LdapUser = u.ldapUsers.GetUserData(u.ldapUsers.GetUserDNByMail(user.Email))
// set user data
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.IPs = strings.Split(device.IPsStr, ", ")
device.DNS = strings.Split(device.DNSStr, ", ")
@ -442,35 +424,35 @@ func (u *UserManager) populateDeviceData(device *Device) {
device.Interface, _ = u.wg.GetDeviceInfo()
}
func (u *UserManager) GetAllUsers() []Peer {
func (u *PeerManager) GetAllPeers() []Peer {
peers := make([]Peer, 0)
u.db.Find(&peers)
for i := range peers {
u.populateUserData(&peers[i])
u.populatePeerData(&peers[i])
}
return peers
}
func (u *UserManager) GetActiveUsers() []Peer {
func (u *PeerManager) GetActivePeers() []Peer {
peers := make([]Peer, 0)
u.db.Where("deactivated_at IS NULL").Find(&peers)
for i := range peers {
u.populateUserData(&peers[i])
u.populatePeerData(&peers[i])
}
return peers
}
func (u *UserManager) GetFilteredAndSortedUsers(sortKey, sortDirection, search string) []Peer {
func (u *PeerManager) GetFilteredAndSortedPeers(sortKey, sortDirection, search string) []Peer {
peers := make([]Peer, 0)
u.db.Find(&peers)
filteredPeers := make([]Peer, 0, len(peers))
for i := range peers {
u.populateUserData(&peers[i])
u.populatePeerData(&peers[i])
if search == "" ||
strings.Contains(peers[i].Email, search) ||
@ -517,12 +499,12 @@ func (u *UserManager) GetFilteredAndSortedUsers(sortKey, sortDirection, search s
return filteredPeers
}
func (u *UserManager) GetSortedUsersForEmail(sortKey, sortDirection, email string) []Peer {
func (u *PeerManager) GetSortedPeersForEmail(sortKey, sortDirection, email string) []Peer {
peers := make([]Peer, 0)
u.db.Where("email = ?", email).Find(&peers)
for i := range peers {
u.populateUserData(&peers[i])
u.populatePeerData(&peers[i])
}
sort.Slice(peers, func(i, j int) bool {
@ -562,7 +544,7 @@ func (u *UserManager) GetSortedUsersForEmail(sortKey, sortDirection, email strin
return peers
}
func (u *UserManager) GetDevice() Device {
func (u *PeerManager) GetDevice() Device {
devices := make([]Device, 0, 1)
u.db.Find(&devices)
@ -573,24 +555,24 @@ func (u *UserManager) GetDevice() Device {
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{}
u.db.Where("public_key = ?", publicKey).FirstOrInit(&peer)
u.populateUserData(&peer)
u.populatePeerData(&peer)
return peer
}
func (u *UserManager) GetUsersByMail(mail string) []Peer {
func (u *PeerManager) GetPeersByMail(mail string) []Peer {
var peers []Peer
u.db.Where("email = ?", mail).Find(&peers)
for i := range peers {
u.populateUserData(&peers[i])
u.populatePeerData(&peers[i])
}
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.UpdatedAt = time.Now()
peer.CreatedAt = time.Now()
@ -606,7 +588,7 @@ func (u *UserManager) CreateUser(peer Peer) error {
return nil
}
func (u *UserManager) UpdateUser(peer Peer) error {
func (u *PeerManager) UpdatePeer(peer Peer) error {
peer.UpdatedAt = time.Now()
peer.AllowedIPsStr = strings.Join(peer.AllowedIPs, ", ")
peer.IPsStr = strings.Join(peer.IPs, ", ")
@ -620,7 +602,7 @@ func (u *UserManager) UpdateUser(peer Peer) error {
return nil
}
func (u *UserManager) DeleteUser(peer Peer) error {
func (u *PeerManager) DeletePeer(peer Peer) error {
res := u.db.Delete(&peer)
if res.Error != nil {
logrus.Errorf("failed to delete peer: %v", res.Error)
@ -630,7 +612,7 @@ func (u *UserManager) DeleteUser(peer Peer) error {
return nil
}
func (u *UserManager) UpdateDevice(device Device) error {
func (u *PeerManager) UpdateDevice(device Device) error {
device.UpdatedAt = time.Now()
device.AllowedIPsStr = strings.Join(device.AllowedIPs, ", ")
device.IPsStr = strings.Join(device.IPs, ", ")
@ -645,10 +627,10 @@ func (u *UserManager) UpdateDevice(device Device) error {
return nil
}
func (u *UserManager) GetAllReservedIps() ([]string, error) {
func (u *PeerManager) GetAllReservedIps() ([]string, error) {
reservedIps := make([]string, 0)
users := u.GetAllUsers()
for _, user := range users {
peers := u.GetAllPeers()
for _, user := range peers {
for _, cidr := range user.IPs {
if cidr == "" {
continue
@ -677,7 +659,7 @@ func (u *UserManager) GetAllReservedIps() ([]string, error) {
return reservedIps, nil
}
func (u *UserManager) IsIPReserved(cidr string) bool {
func (u *PeerManager) IsIPReserved(cidr string) bool {
reserved, err := u.GetAllReservedIps()
if err != nil {
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
func (u *UserManager) GetAvailableIp(cidr string) (string, error) {
func (u *PeerManager) GetAvailableIp(cidr string) (string, error) {
reserved, err := u.GetAllReservedIps()
if err != nil {
return "", err

View File

@ -4,11 +4,20 @@ import (
"net/http"
"github.com/gin-gonic/gin"
wg_portal "github.com/h44z/wg-portal"
)
func SetupRoutes(s *Server) {
// Startpage
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 := s.server.Group("/auth")
@ -18,7 +27,7 @@ func SetupRoutes(s *Server) {
// Admin routes
admin := s.server.Group("/admin")
admin.Use(s.RequireAuthentication(s.config.AdminLdapGroup))
admin.Use(s.RequireAuthentication("admin"))
admin.GET("/", s.GetAdminIndex)
admin.GET("/device/edit", s.GetAdminEditInterface)
admin.POST("/device/edit", s.PostAdminEditInterface)
@ -34,6 +43,12 @@ func SetupRoutes(s *Server) {
admin.GET("/peer/download", s.GetPeerConfig)
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 := s.server.Group("/user")
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 {
return func(c *gin.Context) {
session := s.getSessionData(c)
session := GetSessionData(c)
if !session.LoggedIn {
// Abort the request with the appropriate error code
@ -55,8 +70,15 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
return
}
if scope != "" && !session.IsAdmin && // admins always have access
!s.ldapUsers.IsInGroup(session.UserName, scope) {
if scope == "admin" && !session.IsAdmin {
// 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
c.Abort()
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"`
}