mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-05 07:56:17 +00:00
Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d165fc0658 | ||
|
cadbe4a090 | ||
|
d516d74d3f | ||
|
c9e7145a5b | ||
|
88278bf677 | ||
|
1c4d47293c | ||
|
27de6e8b8c | ||
|
3ecb0925d6 | ||
|
edfecd536a | ||
|
d794f807ad | ||
|
84e5359977 | ||
|
5ac45b7a4f | ||
|
ab02f656be | ||
|
0d4e12a6c1 | ||
|
9a420d26e1 | ||
|
19e6fa2a1a | ||
|
7b1f59d86a | ||
|
9c8a1df01f | ||
|
87964f8ec4 | ||
|
35513ae994 | ||
|
b6d9814021 | ||
|
97edd103be | ||
|
e052f400aa | ||
|
926733dea4 | ||
|
7042523c54 | ||
|
e65a4a8148 | ||
|
28c2494d88 | ||
|
11b9a567d1 | ||
|
f34594f8d2 | ||
|
46dc6dc2ad | ||
|
2ca1226d50 | ||
|
066f939294 | ||
|
17bc297d77 | ||
|
79e4513edb | ||
|
f793ece922 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,3 +32,5 @@ ssh.key
|
||||
.testCoverage.txt
|
||||
wg_portal.db
|
||||
go.sum
|
||||
swagger.json
|
||||
swagger.yaml
|
@@ -20,11 +20,11 @@ script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d .)
|
||||
- go vet $(go list ./... | grep -v /vendor/)
|
||||
- make build
|
||||
- make ENV_BUILD_IDENTIFIER=$TRAVIS_TAG ENV_BUILD_VERSION=$(echo $TRAVIS_COMMIT | cut -c1-7) build
|
||||
# Switch over GCC to cross compilation (breaks 386, hence why do it here only)
|
||||
- sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross
|
||||
- sudo ln -s /usr/include/asm-generic /usr/include/asm
|
||||
- make build-cross-plat
|
||||
- make ENV_BUILD_IDENTIFIER=$TRAVIS_TAG ENV_BUILD_VERSION=$(echo $TRAVIS_COMMIT | cut -c1-7) build-cross-plat
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
|
37
Dockerfile
37
Dockerfile
@@ -6,6 +6,12 @@
|
||||
######-
|
||||
FROM golang:1.16 as builder
|
||||
|
||||
ARG BUILD_IDENTIFIER
|
||||
ENV ENV_BUILD_IDENTIFIER=$BUILD_IDENTIFIER
|
||||
|
||||
ARG BUILD_VERSION
|
||||
ENV ENV_BUILD_VERSION=$BUILD_VERSION
|
||||
|
||||
RUN mkdir /build
|
||||
|
||||
# Copy the source from the current directory to the Working Directory inside the container
|
||||
@@ -17,28 +23,32 @@ WORKDIR /build
|
||||
# Workaround for failing travis-ci builds
|
||||
RUN rm -rf ~/go; rm -rf go.sum
|
||||
|
||||
# Download dependencies
|
||||
RUN curl -L https://git.prolicht.digital/pub/healthcheck/-/releases/v1.0.1/downloads/binaries/hc -o /build/hc; \
|
||||
chmod +rx /build/hc; \
|
||||
echo "Building version: $ENV_BUILD_IDENTIFIER-$ENV_BUILD_VERSION"
|
||||
|
||||
# Build the Go app
|
||||
RUN go clean -modcache; go mod tidy; make build
|
||||
RUN go clean -modcache; go mod tidy; make build-docker
|
||||
|
||||
######-
|
||||
# Here starts the main image
|
||||
######-
|
||||
FROM debian:buster
|
||||
FROM scratch
|
||||
|
||||
# Setup timezone
|
||||
ENV TZ=Europe/Vienna
|
||||
|
||||
# GOSS for container health checks
|
||||
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/*; \
|
||||
curl -L https://github.com/aelsabbahy/goss/releases/download/$GOSS_VERSION/goss-linux-amd64 -o /usr/local/bin/goss && \
|
||||
chmod +rx /usr/local/bin/goss && \
|
||||
goss --version
|
||||
# Import linux stuff from builder.
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /etc/passwd /etc/passwd
|
||||
COPY --from=builder /etc/group /etc/group
|
||||
|
||||
COPY --from=builder /build/dist/wg-portal-amd64 /app/wgportal
|
||||
COPY --from=builder /build/scripts /app/
|
||||
# Import healthcheck binary
|
||||
COPY --from=builder /build/hc /app/hc
|
||||
|
||||
# Copy binaries
|
||||
COPY --from=builder /build/dist/wgportal /app/wgportal
|
||||
|
||||
# Set the Current Working Directory inside the container
|
||||
WORKDIR /app
|
||||
@@ -46,5 +56,4 @@ WORKDIR /app
|
||||
# Command to run the executable
|
||||
CMD [ "/app/wgportal" ]
|
||||
|
||||
HEALTHCHECK --interval=1m --timeout=10s \
|
||||
CMD /app/docker-healthcheck.sh
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD [ "/app/hc", "http://localhost:11223/health" ]
|
||||
|
13
Makefile
13
Makefile
@@ -18,6 +18,9 @@ build-cross-plat: dep build $(addsuffix -arm,$(addprefix $(BUILDDIR)/,$(BINARIES
|
||||
cp scripts/wg-portal.service $(BUILDDIR)
|
||||
cp scripts/wg-portal.env $(BUILDDIR)
|
||||
|
||||
build-docker: dep
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(GOCMD) build -o $(BUILDDIR)/wgportal -ldflags "-w -s -linkmode external -extldflags \"-static\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -tags netgo cmd/wg-portal/main.go
|
||||
|
||||
dep:
|
||||
$(GOCMD) mod download
|
||||
|
||||
@@ -48,13 +51,17 @@ docker-build:
|
||||
docker-push:
|
||||
docker push $(IMAGE)
|
||||
|
||||
api-docs:
|
||||
cd internal/server; swag init --parseDependency --parseInternal --generalInfo api.go
|
||||
$(GOCMD) fmt internal/server/docs/docs.go
|
||||
|
||||
$(BUILDDIR)/%-amd64: cmd/%/main.go dep phony
|
||||
GOOS=linux GOARCH=amd64 $(GOCMD) build -o $@ $<
|
||||
GOOS=linux GOARCH=amd64 $(GOCMD) build -ldflags "-X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $<
|
||||
|
||||
# On arch-linux install aarch64-linux-gnu-gcc to crosscompile for arm64
|
||||
$(BUILDDIR)/%-arm64: cmd/%/main.go dep phony
|
||||
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -ldflags "-linkmode external -extldflags -static" -o $@ $<
|
||||
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -ldflags "-linkmode external -extldflags \"-static\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $<
|
||||
|
||||
# On arch-linux install arm-linux-gnueabihf-gcc to crosscompile for arm
|
||||
$(BUILDDIR)/%-arm: cmd/%/main.go dep phony
|
||||
CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -ldflags "-linkmode external -extldflags -static" -o $@ $<
|
||||
CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -ldflags "-linkmode external -extldflags \"-static\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $<
|
19
README.md
19
README.md
@@ -30,6 +30,7 @@ It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
|
||||
* One single binary
|
||||
* Can be used with existing WireGuard setups
|
||||
* Support for multiple WireGuard interfaces
|
||||
* REST API for management and client deployment
|
||||
|
||||

|
||||
|
||||
@@ -38,6 +39,8 @@ It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
|
||||
### Docker
|
||||
The easiest way to run WireGuard Portal is to use the Docker image provided.
|
||||
|
||||
HINT: the *latest* tag always refers to the master branch and might contain unstable or incompatible code!
|
||||
|
||||
Docker Compose snippet with some sample configuration values:
|
||||
```
|
||||
version: '3.6'
|
||||
@@ -114,6 +117,7 @@ The following configuration options are available:
|
||||
| ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
||||
| EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. |
|
||||
| CREATE_DEFAULT_PEER | createDefaultPeer | core | false | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. |
|
||||
| SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. |
|
||||
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
|
||||
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
|
||||
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
|
||||
@@ -124,10 +128,12 @@ The following configuration options are available:
|
||||
| DATABASE_PASSWORD | password | database | | The mysql password. |
|
||||
| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. |
|
||||
| EMAIL_PORT | port | email | 25 | The email server port. |
|
||||
| EMAIL_TLS | tls | email | false | Use STARTTLS. |
|
||||
| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. |
|
||||
| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. |
|
||||
| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. |
|
||||
| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. |
|
||||
| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. |
|
||||
| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. |
|
||||
| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. |
|
||||
| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). |
|
||||
| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. |
|
||||
@@ -138,15 +144,14 @@ The following configuration options are available:
|
||||
| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. |
|
||||
| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. |
|
||||
| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. |
|
||||
| LDAP_TYPE | typ | ldap | AD | Either AD or OpenLDAP. |
|
||||
| LDAP_USER_CLASS | userClass | ldap | organizationalPerson | The user class that specifies the LDAP object category of users. |
|
||||
| LDAP_LOGIN_FILTER | loginFilter | ldap | (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) | {{login_identifier}} will be replaced with the login email address. |
|
||||
| LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. |
|
||||
| LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. |
|
||||
| LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. |
|
||||
| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. |
|
||||
| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. |
|
||||
| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. |
|
||||
| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. |
|
||||
| LDAP_ATTR_DISABLED | attrDisabled | ldap | userAccountControl | User status attribute. This attribute is used to detect deactivated users. |
|
||||
| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. |
|
||||
| LOG_JSON | | | false | Format log output as JSON. |
|
||||
| LOG_COLOR | | | true | Colorize log output. |
|
||||
@@ -171,7 +176,6 @@ ldap:
|
||||
user: wireguard@test.test
|
||||
pass: test
|
||||
adminGroup: CN=WireGuardAdmins,CN=Users,DC=test,DC=test
|
||||
typ: AD
|
||||
database:
|
||||
typ: sqlite
|
||||
database: data/wg_portal.db
|
||||
@@ -190,6 +194,11 @@ wg:
|
||||
manageIPAddresses: true
|
||||
```
|
||||
|
||||
### RESTful API
|
||||
WireGuard Portal offers a RESTful API to interact with.
|
||||
The API is documented using OpenAPI 2.0, the Swagger UI can be found
|
||||
under the URL `http://<your wg-portal ip/domain>/swagger/index.html`.
|
||||
|
||||
## What is out of scope
|
||||
|
||||
* Generation or application of any `iptables` or `nftables` rules
|
||||
|
@@ -8,24 +8,26 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.prolicht.digital/pub/healthcheck"
|
||||
"github.com/h44z/wg-portal/internal/server"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var Version = "unknown (local build)"
|
||||
|
||||
func main() {
|
||||
_ = setupLogger(logrus.StandardLogger())
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
|
||||
logrus.Infof("starting WireGuard Portal Server [%s]...", Version)
|
||||
logrus.Infof("starting WireGuard Portal Server [%s]...", server.Version)
|
||||
|
||||
// Context for clean shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// start health check service on port 11223
|
||||
healthcheck.New(healthcheck.WithContext(ctx)).Start()
|
||||
|
||||
service := server.Server{}
|
||||
if err := service.Setup(ctx); err != nil {
|
||||
logrus.Fatalf("setup failed: %v", err)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
version: '3.6'
|
||||
services:
|
||||
wg-portal:
|
||||
image: h44z/wg-portal:latest
|
||||
image: h44z/wg-portal:1.0.6
|
||||
container_name: wg-portal
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
|
13
go.mod
13
go.mod
@@ -3,21 +3,32 @@ module github.com/h44z/wg-portal
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
git.prolicht.digital/pub/healthcheck v1.0.1
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/evanphx/json-patch v0.5.2
|
||||
github.com/gin-contrib/sessions v0.0.3
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/go-ldap/ldap/v3 v3.2.4
|
||||
github.com/go-openapi/spec v0.20.3 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/milosgajdos/tenus v0.0.3
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/swaggo/gin-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.7.0
|
||||
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
|
||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
|
||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
|
||||
github.com/xhit/go-simple-mail/v2 v2.8.1
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 // indirect
|
||||
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 // indirect
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gorm.io/driver/mysql v1.0.5
|
||||
|
10
hooks/build
Executable file
10
hooks/build
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# File needs to be called /hooks/build relative to the Dockerfile.
|
||||
# Some environment variables are injected into the build hook, see: https://docs.docker.com/docker-hub/builds/advanced/.
|
||||
|
||||
GIT_SHORT_HASH=$(echo $SOURCE_COMMIT | cut -c1-7)
|
||||
echo "Build hook running for git hash $GIT_SHORT_HASH"
|
||||
docker build --build-arg BUILD_IDENTIFIER=$DOCKER_TAG \
|
||||
--build-arg BUILD_VERSION=$GIT_SHORT_HASH \
|
||||
-t $IMAGE_NAME .
|
@@ -2,7 +2,6 @@ package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -69,13 +68,11 @@ func (provider Provider) Login(ctx *authentication.AuthContext) (string, error)
|
||||
|
||||
// Search for the given username
|
||||
attrs := []string{"dn", provider.config.EmailAttribute}
|
||||
if provider.config.DisabledAttribute != "" {
|
||||
attrs = append(attrs, provider.config.DisabledAttribute)
|
||||
}
|
||||
loginFilter := strings.Replace(provider.config.LoginFilter, "{{login_identifier}}", username, -1)
|
||||
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),
|
||||
loginFilter,
|
||||
attrs,
|
||||
nil,
|
||||
)
|
||||
@@ -89,24 +86,8 @@ func (provider Provider) Login(ctx *authentication.AuthContext) (string, error)
|
||||
return "", errors.Errorf("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.New("user is disabled")
|
||||
}
|
||||
case ldapconfig.TypeOpenLDAP:
|
||||
if ldapconfig.IsOpenLdapUserDisabled(uac) {
|
||||
return "", errors.New("user is disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bind as the user to verify their password
|
||||
userDN := sr.Entries[0].DN
|
||||
err = client.Bind(userDN, password)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "invalid credentials")
|
||||
@@ -136,13 +117,11 @@ func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authent
|
||||
// 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)
|
||||
}
|
||||
loginFilter := strings.Replace(provider.config.LoginFilter, "{{login_identifier}}", username, -1)
|
||||
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),
|
||||
loginFilter,
|
||||
attrs,
|
||||
nil,
|
||||
)
|
||||
@@ -175,14 +154,15 @@ func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authent
|
||||
}
|
||||
|
||||
func (provider Provider) open() (*ldap.Conn, error) {
|
||||
conn, err := ldap.DialURL(provider.config.URL)
|
||||
tlsConfig := &tls.Config{InsecureSkipVerify: !provider.config.CertValidation}
|
||||
conn, err := ldap.DialURL(provider.config.URL, ldap.DialWithTLSConfig(tlsConfig))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if provider.config.StartTLS {
|
||||
// Reconnect with TLS
|
||||
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !provider.config.CertValidation})
|
||||
err = conn.StartTLS(tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -7,10 +7,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/common"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/authentication"
|
||||
"github.com/h44z/wg-portal/internal/common"
|
||||
"github.com/h44z/wg-portal/internal/users"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -109,6 +108,7 @@ func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authent
|
||||
}
|
||||
|
||||
func (provider Provider) InitializeAdmin(email, password string) error {
|
||||
email = strings.ToLower(email)
|
||||
if !emailRegex.MatchString(email) {
|
||||
return errors.New("admin username must be an email address")
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func (provider Provider) InitializeAdmin(email, password string) error {
|
||||
}
|
||||
|
||||
admin.Email = email
|
||||
admin.Password = string(hashedPassword)
|
||||
admin.Password = users.PrivateString(hashedPassword)
|
||||
admin.Firstname = "WireGuard"
|
||||
admin.Lastname = "Administrator"
|
||||
admin.CreatedAt = time.Now()
|
||||
@@ -170,7 +170,7 @@ func (provider Provider) InitializeAdmin(email, password string) error {
|
||||
return errors.Wrap(err, "failed to hash admin password")
|
||||
}
|
||||
|
||||
admin.Password = string(hashedPassword)
|
||||
admin.Password = users.PrivateString(hashedPassword)
|
||||
admin.IsAdmin = true
|
||||
admin.UpdatedAt = time.Now()
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -14,6 +15,22 @@ import (
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, Migration{
|
||||
version: "1.0.7",
|
||||
migrateFn: func(db *gorm.DB) error {
|
||||
if err := db.Exec("UPDATE users SET email = LOWER(email)").Error; err != nil {
|
||||
return errors.Wrap(err, "failed to convert user emails to lower case")
|
||||
}
|
||||
if err := db.Exec("UPDATE peers SET email = LOWER(email)").Error; err != nil {
|
||||
return errors.Wrap(err, "failed to convert peer emails to lower case")
|
||||
}
|
||||
logrus.Infof("upgraded database format to version 1.0.7")
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type SupportedDatabase string
|
||||
|
||||
const (
|
||||
@@ -80,16 +97,18 @@ type DatabaseMigrationInfo struct {
|
||||
Applied time.Time
|
||||
}
|
||||
|
||||
type Migration struct {
|
||||
version string
|
||||
migrateFn func(db *gorm.DB) error
|
||||
}
|
||||
|
||||
var migrations []Migration
|
||||
|
||||
func MigrateDatabase(db *gorm.DB, version string) error {
|
||||
if err := db.AutoMigrate(&DatabaseMigrationInfo{}); err != nil {
|
||||
return errors.Wrap(err, "failed to migrate version database")
|
||||
}
|
||||
|
||||
newVersion := DatabaseMigrationInfo{
|
||||
Version: version,
|
||||
Applied: time.Now(),
|
||||
}
|
||||
|
||||
existingMigration := DatabaseMigrationInfo{}
|
||||
db.Where("version = ?", version).FirstOrInit(&existingMigration)
|
||||
|
||||
@@ -97,11 +116,36 @@ func MigrateDatabase(db *gorm.DB, version string) error {
|
||||
lastVersion := DatabaseMigrationInfo{}
|
||||
db.Order("applied desc, version desc").FirstOrInit(&lastVersion)
|
||||
|
||||
// TODO: migrate database
|
||||
if lastVersion.Version == "" {
|
||||
// fresh database, no migrations to apply
|
||||
res := db.Create(&DatabaseMigrationInfo{
|
||||
Version: version,
|
||||
Applied: time.Now(),
|
||||
})
|
||||
if res.Error != nil {
|
||||
return errors.Wrapf(res.Error, "failed to write version %s to database", version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
res := db.Create(&newVersion)
|
||||
if res.Error != nil {
|
||||
return errors.Wrap(res.Error, "failed to write version to database")
|
||||
sort.Slice(migrations, func(i, j int) bool {
|
||||
return migrations[i].version < migrations[j].version
|
||||
})
|
||||
|
||||
for _, migration := range migrations {
|
||||
if migration.version > lastVersion.Version {
|
||||
if err := migration.migrateFn(db); err != nil {
|
||||
return errors.Wrapf(err, "failed to migrate to version %s", migration.version)
|
||||
}
|
||||
|
||||
res := db.Create(&DatabaseMigrationInfo{
|
||||
Version: migration.version,
|
||||
Applied: time.Now(),
|
||||
})
|
||||
if res.Error != nil {
|
||||
return errors.Wrapf(res.Error, "failed to write version %s to database", migration.version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,20 +3,38 @@ package common
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/jordan-wright/email"
|
||||
"github.com/pkg/errors"
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
)
|
||||
|
||||
type MailEncryption string
|
||||
|
||||
const (
|
||||
MailEncryptionNone MailEncryption = "none"
|
||||
MailEncryptionTLS MailEncryption = "tls"
|
||||
MailEncryptionStartTLS MailEncryption = "starttls"
|
||||
)
|
||||
|
||||
type MailAuthType string
|
||||
|
||||
const (
|
||||
MailAuthPlain MailAuthType = "plain"
|
||||
MailAuthLogin MailAuthType = "login"
|
||||
MailAuthCramMD5 MailAuthType = "crammd5"
|
||||
)
|
||||
|
||||
type MailConfig struct {
|
||||
Host string `yaml:"host" envconfig:"EMAIL_HOST"`
|
||||
Port int `yaml:"port" envconfig:"EMAIL_PORT"`
|
||||
TLS bool `yaml:"tls" envconfig:"EMAIL_TLS"`
|
||||
CertValidation bool `yaml:"certcheck" envconfig:"EMAIL_CERT_VALIDATION"`
|
||||
Username string `yaml:"user" envconfig:"EMAIL_USERNAME"`
|
||||
Password string `yaml:"pass" envconfig:"EMAIL_PASSWORD"`
|
||||
Host string `yaml:"host" envconfig:"EMAIL_HOST"`
|
||||
Port int `yaml:"port" envconfig:"EMAIL_PORT"`
|
||||
TLS bool `yaml:"tls" envconfig:"EMAIL_TLS"` // Deprecated, use MailConfig.Encryption instead.
|
||||
Encryption MailEncryption `yaml:"encryption" envconfig:"EMAIL_ENCRYPTION"`
|
||||
CertValidation bool `yaml:"certcheck" envconfig:"EMAIL_CERT_VALIDATION"`
|
||||
Username string `yaml:"user" envconfig:"EMAIL_USERNAME"`
|
||||
Password string `yaml:"pass" envconfig:"EMAIL_PASSWORD"`
|
||||
AuthType MailAuthType `yaml:"auth" envconfig:"EMAIL_AUTHTYPE"`
|
||||
}
|
||||
|
||||
type MailAttachment struct {
|
||||
@@ -27,53 +45,73 @@ type MailAttachment struct {
|
||||
}
|
||||
|
||||
// SendEmailWithAttachments sends a mail with optional attachments.
|
||||
func SendEmailWithAttachments(cfg MailConfig, sender, replyTo, subject, body string, htmlBody string, receivers []string, attachments []MailAttachment) error {
|
||||
e := email.NewEmail()
|
||||
func SendEmailWithAttachments(cfg MailConfig, sender, replyTo, subject, body, htmlBody string, receivers []string, attachments []MailAttachment) error {
|
||||
srv := mail.NewSMTPClient()
|
||||
|
||||
srv.ConnectTimeout = 30 * time.Second
|
||||
srv.SendTimeout = 30 * time.Second
|
||||
srv.Host = cfg.Host
|
||||
srv.Port = cfg.Port
|
||||
srv.Username = cfg.Username
|
||||
srv.Password = cfg.Password
|
||||
|
||||
// TODO: remove this once the deprecated MailConfig.TLS config option has been removed
|
||||
if cfg.TLS {
|
||||
cfg.Encryption = MailEncryptionStartTLS
|
||||
}
|
||||
switch cfg.Encryption {
|
||||
case MailEncryptionTLS:
|
||||
srv.Encryption = mail.EncryptionSSLTLS
|
||||
case MailEncryptionStartTLS:
|
||||
srv.Encryption = mail.EncryptionSTARTTLS
|
||||
default: // MailEncryptionNone
|
||||
srv.Encryption = mail.EncryptionNone
|
||||
}
|
||||
srv.TLSConfig = &tls.Config{ServerName: srv.Host, InsecureSkipVerify: !cfg.CertValidation}
|
||||
switch cfg.AuthType {
|
||||
case MailAuthPlain:
|
||||
srv.Authentication = mail.AuthPlain
|
||||
case MailAuthLogin:
|
||||
srv.Authentication = mail.AuthLogin
|
||||
case MailAuthCramMD5:
|
||||
srv.Authentication = mail.AuthCRAMMD5
|
||||
}
|
||||
|
||||
client, err := srv.Connect()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect via SMTP")
|
||||
}
|
||||
|
||||
hostname := cfg.Host + ":" + strconv.Itoa(cfg.Port)
|
||||
subject = strings.Trim(subject, "\n\r\t")
|
||||
sender = strings.Trim(sender, "\n\r\t")
|
||||
replyTo = strings.Trim(replyTo, "\n\r\t")
|
||||
if replyTo == "" {
|
||||
replyTo = sender
|
||||
}
|
||||
|
||||
var auth smtp.Auth
|
||||
if cfg.Username == "" {
|
||||
auth = nil
|
||||
} else {
|
||||
// Set up authentication information.
|
||||
auth = smtp.PlainAuth(
|
||||
"",
|
||||
cfg.Username,
|
||||
cfg.Password,
|
||||
cfg.Host,
|
||||
)
|
||||
}
|
||||
email := mail.NewMSG()
|
||||
email.SetFrom(sender).
|
||||
AddTo(receivers...).
|
||||
SetReplyTo(replyTo).
|
||||
SetSubject(subject)
|
||||
|
||||
// Set email data.
|
||||
e.From = sender
|
||||
e.To = receivers
|
||||
e.ReplyTo = []string{replyTo}
|
||||
e.Subject = subject
|
||||
e.Text = []byte(body)
|
||||
if htmlBody != "" {
|
||||
e.HTML = []byte(htmlBody)
|
||||
}
|
||||
email.SetBody(mail.TextHTML, htmlBody)
|
||||
email.AddAlternative(mail.TextPlain, body)
|
||||
|
||||
for _, attachment := range attachments {
|
||||
a, err := e.Attach(attachment.Data, attachment.Name, attachment.ContentType)
|
||||
attachmentData, err := ioutil.ReadAll(attachment.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "failed to read attachment data for %s", attachment.Name)
|
||||
}
|
||||
|
||||
if attachment.Embedded {
|
||||
a.HTMLRelated = true
|
||||
email.AddInlineData(attachmentData, attachment.Name, attachment.ContentType)
|
||||
} else {
|
||||
email.AddAttachmentData(attachmentData, attachment.Name, attachment.ContentType)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.TLS {
|
||||
return e.SendWithStartTLS(hostname, auth, &tls.Config{InsecureSkipVerify: !cfg.CertValidation})
|
||||
} else {
|
||||
return e.Send(hostname, auth)
|
||||
// Call Send and pass the client
|
||||
err = email.Send(client)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to send email")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@@ -15,14 +15,13 @@ type Config struct {
|
||||
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"`
|
||||
|
||||
LoginFilter string `yaml:"loginFilter" envconfig:"LDAP_LOGIN_FILTER"` // {{login_identifier}} gets replaced with the login email address
|
||||
SyncFilter string `yaml:"syncFilter" envconfig:"LDAP_SYNC_FILTER"`
|
||||
AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"` // Members of this group receive admin rights in WG-Portal
|
||||
}
|
||||
|
@@ -2,8 +2,6 @@ package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/pkg/errors"
|
||||
@@ -16,14 +14,15 @@ type RawLdapData struct {
|
||||
}
|
||||
|
||||
func Open(cfg *Config) (*ldap.Conn, error) {
|
||||
conn, err := ldap.DialURL(cfg.URL)
|
||||
tlsConfig := &tls.Config{InsecureSkipVerify: !cfg.CertValidation}
|
||||
conn, err := ldap.DialURL(cfg.URL, ldap.DialWithTLSConfig(tlsConfig))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to LDAP")
|
||||
}
|
||||
|
||||
if cfg.StartTLS {
|
||||
// Reconnect with TLS
|
||||
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !cfg.CertValidation})
|
||||
err = conn.StartTLS(tlsConfig)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to star TLS on connection")
|
||||
}
|
||||
@@ -53,13 +52,10 @@ func FindAllUsers(cfg *Config) ([]RawLdapData, error) {
|
||||
// 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,
|
||||
cfg.SyncFilter, attrs, nil,
|
||||
)
|
||||
|
||||
sr, err := client.Search(searchRequest)
|
||||
@@ -86,27 +82,3 @@ func FindAllUsers(cfg *Config) ([]RawLdapData, error) {
|
||||
|
||||
return tmpData, nil
|
||||
}
|
||||
|
||||
func IsActiveDirectoryUserDisabled(userAccountControl string) bool {
|
||||
if userAccountControl == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
uacInt, err := strconv.ParseInt(userAccountControl, 10, 32)
|
||||
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
|
||||
}
|
||||
|
926
internal/server/api.go
Normal file
926
internal/server/api.go
Normal file
@@ -0,0 +1,926 @@
|
||||
package server
|
||||
|
||||
// go get -u github.com/swaggo/swag/cmd/swag
|
||||
// run: swag init --parseDependency --parseInternal --generalInfo api.go
|
||||
// in the internal/server folder
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/common"
|
||||
"github.com/h44z/wg-portal/internal/users"
|
||||
"github.com/h44z/wg-portal/internal/wireguard"
|
||||
)
|
||||
|
||||
// @title WireGuard Portal API
|
||||
// @version 1.0
|
||||
// @description WireGuard Portal API for managing users and peers.
|
||||
|
||||
// @license.name MIT
|
||||
// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
|
||||
|
||||
// @contact.name WireGuard Portal Project
|
||||
// @contact.url https://github.com/h44z/wg-portal
|
||||
|
||||
// @securityDefinitions.basic ApiBasicAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @scope.admin Admin access required
|
||||
|
||||
// @securityDefinitions.basic GeneralBasicAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @scope.user User access required
|
||||
|
||||
// @BasePath /api/v1
|
||||
|
||||
// ApiServer is a simple wrapper struct so that we can have fresh member function names.
|
||||
type ApiServer struct {
|
||||
s *Server
|
||||
}
|
||||
|
||||
type ApiError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
// GetUsers godoc
|
||||
// @Tags Users
|
||||
// @Summary Retrieves all users
|
||||
// @Produce json
|
||||
// @Success 200 {object} []users.User
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Router /backend/users [get]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) GetUsers(c *gin.Context) {
|
||||
allUsers := s.s.users.GetUsersUnscoped()
|
||||
|
||||
c.JSON(http.StatusOK, allUsers)
|
||||
}
|
||||
|
||||
// GetUser godoc
|
||||
// @Tags Users
|
||||
// @Summary Retrieves user based on given Email
|
||||
// @Produce json
|
||||
// @Param email query string true "User Email"
|
||||
// @Success 200 {object} users.User
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Router /backend/user [get]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) GetUser(c *gin.Context) {
|
||||
email := strings.ToLower(strings.TrimSpace(c.Query("email")))
|
||||
if email == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
user := s.s.users.GetUserUnscoped(email)
|
||||
if user == nil {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// PostUser godoc
|
||||
// @Tags Users
|
||||
// @Summary Creates a new user based on the given user model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user body users.User true "User Model"
|
||||
// @Success 200 {object} users.User
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Failure 500 {object} ApiError
|
||||
// @Router /backend/users [post]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) PostUser(c *gin.Context) {
|
||||
newUser := users.User{}
|
||||
if err := c.BindJSON(&newUser); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if user := s.s.users.GetUserUnscoped(newUser.Email); user != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "user already exists"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.s.CreateUser(newUser, s.s.wg.Cfg.GetDefaultDeviceName()); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user := s.s.users.GetUserUnscoped(newUser.Email)
|
||||
if user == nil {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// PutUser godoc
|
||||
// @Tags Users
|
||||
// @Summary Updates a user based on the given user model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param email query string true "User Email"
|
||||
// @Param user body users.User true "User Model"
|
||||
// @Success 200 {object} users.User
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Failure 500 {object} ApiError
|
||||
// @Router /backend/user [put]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) PutUser(c *gin.Context) {
|
||||
email := strings.ToLower(strings.TrimSpace(c.Query("email")))
|
||||
if email == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
updateUser := users.User{}
|
||||
if err := c.BindJSON(&updateUser); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Changing email address is not allowed
|
||||
if email != updateUser.Email {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"})
|
||||
return
|
||||
}
|
||||
|
||||
if user := s.s.users.GetUserUnscoped(email); user == nil {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.s.UpdateUser(updateUser); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user := s.s.users.GetUserUnscoped(email)
|
||||
if user == nil {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// PatchUser godoc
|
||||
// @Tags Users
|
||||
// @Summary Updates a user based on the given partial user model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param email query string true "User Email"
|
||||
// @Param user body users.User true "User Model"
|
||||
// @Success 200 {object} users.User
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Failure 500 {object} ApiError
|
||||
// @Router /backend/user [patch]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) PatchUser(c *gin.Context) {
|
||||
email := strings.ToLower(strings.TrimSpace(c.Query("email")))
|
||||
if email == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
patch, err := c.GetRawData()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user := s.s.users.GetUserUnscoped(email)
|
||||
if user == nil {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"})
|
||||
return
|
||||
}
|
||||
userData, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
mergedUserData, err := jsonpatch.MergePatch(userData, patch)
|
||||
var mergedUser users.User
|
||||
err = json.Unmarshal(mergedUserData, &mergedUser)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// CHanging email address is not allowed
|
||||
if email != mergedUser.Email {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.s.UpdateUser(mergedUser); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user = s.s.users.GetUserUnscoped(email)
|
||||
if user == nil {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// DeleteUser godoc
|
||||
// @Tags Users
|
||||
// @Summary Deletes the specified user
|
||||
// @Produce json
|
||||
// @Param email query string true "User Email"
|
||||
// @Success 204 "No content"
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Failure 500 {object} ApiError
|
||||
// @Router /backend/user [delete]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) DeleteUser(c *gin.Context) {
|
||||
email := strings.ToLower(strings.TrimSpace(c.Query("email")))
|
||||
if email == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
var user *users.User
|
||||
if user = s.s.users.GetUserUnscoped(email); user == nil {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.s.DeleteUser(*user); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetPeers godoc
|
||||
// @Tags Peers
|
||||
// @Summary Retrieves all peers for the given interface
|
||||
// @Produce json
|
||||
// @Param device query string true "Device Name"
|
||||
// @Success 200 {object} []wireguard.Peer
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Router /backend/peers [get]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) GetPeers(c *gin.Context) {
|
||||
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device")))
|
||||
if deviceName == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
// validate device name
|
||||
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
|
||||
return
|
||||
}
|
||||
|
||||
peers := s.s.peers.GetAllPeers(deviceName)
|
||||
c.JSON(http.StatusOK, peers)
|
||||
}
|
||||
|
||||
// GetPeer godoc
|
||||
// @Tags Peers
|
||||
// @Summary Retrieves the peer for the given public key
|
||||
// @Produce json
|
||||
// @Param pkey query string true "Public Key (Base 64)"
|
||||
// @Success 200 {object} wireguard.Peer
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Router /backend/peer [get]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) GetPeer(c *gin.Context) {
|
||||
pkey := c.Query("pkey")
|
||||
if pkey == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
peer := s.s.peers.GetPeerByKey(pkey)
|
||||
if !peer.IsValid() {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, peer)
|
||||
}
|
||||
|
||||
// PostPeer godoc
|
||||
// @Tags Peers
|
||||
// @Summary Creates a new peer based on the given peer model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param device query string true "Device Name"
|
||||
// @Param peer body wireguard.Peer true "Peer Model"
|
||||
// @Success 200 {object} wireguard.Peer
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Failure 500 {object} ApiError
|
||||
// @Router /backend/peers [post]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) PostPeer(c *gin.Context) {
|
||||
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device")))
|
||||
if deviceName == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
// validate device name
|
||||
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
|
||||
return
|
||||
}
|
||||
|
||||
newPeer := wireguard.Peer{}
|
||||
if err := c.BindJSON(&newPeer); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if peer := s.s.peers.GetPeerByKey(newPeer.PublicKey); peer.IsValid() {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "peer already exists"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.s.CreatePeer(deviceName, newPeer); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
peer := s.s.peers.GetPeerByKey(newPeer.PublicKey)
|
||||
if !peer.IsValid() {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, peer)
|
||||
}
|
||||
|
||||
// PutPeer godoc
|
||||
// @Tags Peers
|
||||
// @Summary Updates the given peer based on the given peer model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param pkey query string true "Public Key"
|
||||
// @Param peer body wireguard.Peer true "Peer Model"
|
||||
// @Success 200 {object} wireguard.Peer
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Failure 500 {object} ApiError
|
||||
// @Router /backend/peer [put]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) PutPeer(c *gin.Context) {
|
||||
updatePeer := wireguard.Peer{}
|
||||
if err := c.BindJSON(&updatePeer); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
pkey := c.Query("pkey")
|
||||
if pkey == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
if peer := s.s.peers.GetPeerByKey(pkey); !peer.IsValid() {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
|
||||
return
|
||||
}
|
||||
|
||||
// Changing public key is not allowed
|
||||
if pkey != updatePeer.PublicKey {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must match the model public key"})
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if updatePeer.DeactivatedAt != nil {
|
||||
updatePeer.DeactivatedAt = &now
|
||||
}
|
||||
if err := s.s.UpdatePeer(updatePeer, now); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
peer := s.s.peers.GetPeerByKey(updatePeer.PublicKey)
|
||||
if !peer.IsValid() {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, peer)
|
||||
}
|
||||
|
||||
// PatchPeer godoc
|
||||
// @Tags Peers
|
||||
// @Summary Updates the given peer based on the given partial peer model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param pkey query string true "Public Key"
|
||||
// @Param peer body wireguard.Peer true "Peer Model"
|
||||
// @Success 200 {object} wireguard.Peer
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Failure 500 {object} ApiError
|
||||
// @Router /backend/peer [patch]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) PatchPeer(c *gin.Context) {
|
||||
patch, err := c.GetRawData()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
pkey := c.Query("pkey")
|
||||
if pkey == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
peer := s.s.peers.GetPeerByKey(pkey)
|
||||
if !peer.IsValid() {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
|
||||
return
|
||||
}
|
||||
|
||||
peerData, err := json.Marshal(peer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
mergedPeerData, err := jsonpatch.MergePatch(peerData, patch)
|
||||
var mergedPeer wireguard.Peer
|
||||
err = json.Unmarshal(mergedPeerData, &mergedPeer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if !mergedPeer.IsValid() {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "invalid peer model"})
|
||||
return
|
||||
}
|
||||
|
||||
// Changing public key is not allowed
|
||||
if pkey != mergedPeer.PublicKey {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must match the model public key"})
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if mergedPeer.DeactivatedAt != nil {
|
||||
mergedPeer.DeactivatedAt = &now
|
||||
}
|
||||
if err := s.s.UpdatePeer(mergedPeer, now); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
peer = s.s.peers.GetPeerByKey(mergedPeer.PublicKey)
|
||||
if !peer.IsValid() {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, peer)
|
||||
}
|
||||
|
||||
// DeletePeer godoc
|
||||
// @Tags Peers
|
||||
// @Summary Updates the given peer based on the given partial peer model
|
||||
// @Produce json
|
||||
// @Param pkey query string true "Public Key"
|
||||
// @Success 202 "No Content"
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Failure 500 {object} ApiError
|
||||
// @Router /backend/peer [delete]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) DeletePeer(c *gin.Context) {
|
||||
pkey := c.Query("pkey")
|
||||
if pkey == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
peer := s.s.peers.GetPeerByKey(pkey)
|
||||
if peer.PublicKey == "" {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.s.DeletePeer(peer); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetDevices godoc
|
||||
// @Tags Interface
|
||||
// @Summary Get all devices
|
||||
// @Produce json
|
||||
// @Success 200 {object} []wireguard.Device
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Router /backend/devices [get]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) GetDevices(c *gin.Context) {
|
||||
var devices []wireguard.Device
|
||||
for _, deviceName := range s.s.config.WG.DeviceNames {
|
||||
device := s.s.peers.GetDevice(deviceName)
|
||||
if !device.IsValid() {
|
||||
continue
|
||||
}
|
||||
devices = append(devices, device)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, devices)
|
||||
}
|
||||
|
||||
// GetDevice godoc
|
||||
// @Tags Interface
|
||||
// @Summary Get the given device
|
||||
// @Produce json
|
||||
// @Param device query string true "Device Name"
|
||||
// @Success 200 {object} wireguard.Device
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Router /backend/device [get]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) GetDevice(c *gin.Context) {
|
||||
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device")))
|
||||
if deviceName == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
// validate device name
|
||||
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
|
||||
return
|
||||
}
|
||||
|
||||
device := s.s.peers.GetDevice(deviceName)
|
||||
if !device.IsValid() {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "device not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, device)
|
||||
}
|
||||
|
||||
// PutDevice godoc
|
||||
// @Tags Interface
|
||||
// @Summary Updates the given device based on the given device model (UNIMPLEMENTED)
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param device query string true "Device Name"
|
||||
// @Param body body wireguard.Device true "Device Model"
|
||||
// @Success 200 {object} wireguard.Device
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Failure 500 {object} ApiError
|
||||
// @Router /backend/device [put]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) PutDevice(c *gin.Context) {
|
||||
updateDevice := wireguard.Device{}
|
||||
if err := c.BindJSON(&updateDevice); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device")))
|
||||
if deviceName == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
// validate device name
|
||||
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
|
||||
return
|
||||
}
|
||||
|
||||
device := s.s.peers.GetDevice(deviceName)
|
||||
if !device.IsValid() {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Changing device name is not allowed
|
||||
if deviceName != updateDevice.DeviceName {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must match the model device name"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: implement
|
||||
|
||||
c.JSON(http.StatusNotImplemented, device)
|
||||
}
|
||||
|
||||
// PatchDevice godoc
|
||||
// @Tags Interface
|
||||
// @Summary Updates the given device based on the given partial device model (UNIMPLEMENTED)
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param device query string true "Device Name"
|
||||
// @Param body body wireguard.Device true "Device Model"
|
||||
// @Success 200 {object} wireguard.Device
|
||||
// @Failure 400 {object} ApiError
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Failure 500 {object} ApiError
|
||||
// @Router /backend/device [patch]
|
||||
// @Security ApiBasicAuth
|
||||
func (s *ApiServer) PatchDevice(c *gin.Context) {
|
||||
patch, err := c.GetRawData()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
deviceName := strings.ToLower(strings.TrimSpace(c.Query("device")))
|
||||
if deviceName == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
// validate device name
|
||||
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
|
||||
return
|
||||
}
|
||||
|
||||
device := s.s.peers.GetDevice(deviceName)
|
||||
if !device.IsValid() {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
|
||||
return
|
||||
}
|
||||
|
||||
deviceData, err := json.Marshal(device)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
mergedDeviceData, err := jsonpatch.MergePatch(deviceData, patch)
|
||||
var mergedDevice wireguard.Device
|
||||
err = json.Unmarshal(mergedDeviceData, &mergedDevice)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if !mergedDevice.IsValid() {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "invalid device model"})
|
||||
return
|
||||
}
|
||||
|
||||
// Changing device name is not allowed
|
||||
if deviceName != mergedDevice.DeviceName {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must match the model device name"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: implement
|
||||
|
||||
c.JSON(http.StatusNotImplemented, device)
|
||||
}
|
||||
|
||||
type PeerDeploymentInformation struct {
|
||||
PublicKey string
|
||||
Identifier string
|
||||
Device string
|
||||
DeviceIdentifier string
|
||||
}
|
||||
|
||||
// GetPeerDeploymentInformation godoc
|
||||
// @Tags Provisioning
|
||||
// @Summary Retrieves all active peers for the given email address
|
||||
// @Produce json
|
||||
// @Param email query string true "Email Address"
|
||||
// @Success 200 {object} []PeerDeploymentInformation "All active WireGuard peers"
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Router /provisioning/peers [get]
|
||||
// @Security GeneralBasicAuth
|
||||
func (s *ApiServer) GetPeerDeploymentInformation(c *gin.Context) {
|
||||
email := c.Query("email")
|
||||
if email == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get authenticated user to check permissions
|
||||
username, _, _ := c.Request.BasicAuth()
|
||||
user := s.s.users.GetUser(username)
|
||||
|
||||
if !user.IsAdmin && user.Email != email {
|
||||
c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
|
||||
return
|
||||
}
|
||||
|
||||
peers := s.s.peers.GetPeersByMail(email)
|
||||
result := make([]PeerDeploymentInformation, 0, len(peers))
|
||||
for i := range peers {
|
||||
if peers[i].DeactivatedAt != nil {
|
||||
continue // skip deactivated peers
|
||||
}
|
||||
|
||||
device := s.s.peers.GetDevice(peers[i].DeviceName)
|
||||
if device.Type != wireguard.DeviceTypeServer {
|
||||
continue // Skip peers on non-server devices
|
||||
}
|
||||
|
||||
result = append(result, PeerDeploymentInformation{
|
||||
PublicKey: peers[i].PublicKey,
|
||||
Identifier: peers[i].Identifier,
|
||||
Device: device.DeviceName,
|
||||
DeviceIdentifier: device.DisplayName,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetPeerDeploymentConfig godoc
|
||||
// @Tags Provisioning
|
||||
// @Summary Retrieves the peer config for the given public key
|
||||
// @Produce plain
|
||||
// @Param pkey query string true "Public Key (Base 64)"
|
||||
// @Success 200 {object} string "The WireGuard configuration file"
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Router /provisioning/peer [get]
|
||||
// @Security GeneralBasicAuth
|
||||
func (s *ApiServer) GetPeerDeploymentConfig(c *gin.Context) {
|
||||
pkey := c.Query("pkey")
|
||||
if pkey == "" {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
|
||||
return
|
||||
}
|
||||
|
||||
peer := s.s.peers.GetPeerByKey(pkey)
|
||||
if !peer.IsValid() {
|
||||
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get authenticated user to check permissions
|
||||
username, _, _ := c.Request.BasicAuth()
|
||||
user := s.s.users.GetUser(username)
|
||||
|
||||
if !user.IsAdmin && user.Email != peer.Email {
|
||||
c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
|
||||
return
|
||||
}
|
||||
|
||||
device := s.s.peers.GetDevice(peer.DeviceName)
|
||||
config, err := peer.GetConfigFile(device)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "text/plain", config)
|
||||
}
|
||||
|
||||
type ProvisioningRequest struct {
|
||||
// DeviceName is optional, if not specified, the configured default device will be used.
|
||||
DeviceName string `json:",omitempty"`
|
||||
Identifier string `binding:"required"`
|
||||
Email string `binding:"required"`
|
||||
|
||||
// Client specific and optional settings
|
||||
|
||||
AllowedIPsStr string `binding:"cidrlist" json:",omitempty"`
|
||||
PersistentKeepalive int `binding:"gte=0" json:",omitempty"`
|
||||
DNSStr string `binding:"iplist" json:",omitempty"`
|
||||
Mtu int `binding:"gte=0,lte=1500" json:",omitempty"`
|
||||
}
|
||||
|
||||
// PostPeerDeploymentConfig godoc
|
||||
// @Tags Provisioning
|
||||
// @Summary Creates the requested peer config and returns the config file
|
||||
// @Accept json
|
||||
// @Produce plain
|
||||
// @Param body body ProvisioningRequest true "Provisioning Request Model"
|
||||
// @Success 200 {object} string "The WireGuard configuration file"
|
||||
// @Failure 401 {object} ApiError
|
||||
// @Failure 403 {object} ApiError
|
||||
// @Failure 404 {object} ApiError
|
||||
// @Router /provisioning/peers [post]
|
||||
// @Security GeneralBasicAuth
|
||||
func (s *ApiServer) PostPeerDeploymentConfig(c *gin.Context) {
|
||||
req := ProvisioningRequest{}
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get authenticated user to check permissions
|
||||
username, _, _ := c.Request.BasicAuth()
|
||||
user := s.s.users.GetUser(username)
|
||||
|
||||
if !user.IsAdmin && !s.s.config.Core.SelfProvisioningAllowed {
|
||||
c.JSON(http.StatusForbidden, ApiError{Message: "peer provisioning service disabled"})
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsAdmin && user.Email != req.Email {
|
||||
c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
|
||||
return
|
||||
}
|
||||
|
||||
deviceName := req.DeviceName
|
||||
if deviceName == "" || !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||
deviceName = s.s.config.WG.GetDefaultDeviceName()
|
||||
}
|
||||
device := s.s.peers.GetDevice(deviceName)
|
||||
if device.Type != wireguard.DeviceTypeServer {
|
||||
c.JSON(http.StatusForbidden, ApiError{Message: "invalid device, provisioning disabled"})
|
||||
return
|
||||
}
|
||||
|
||||
// check if private/public keys are set, if so check database for existing entries
|
||||
peer, err := s.s.PrepareNewPeer(deviceName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
peer.Email = req.Email
|
||||
peer.Identifier = req.Identifier
|
||||
|
||||
if req.AllowedIPsStr != "" {
|
||||
peer.AllowedIPsStr = req.AllowedIPsStr
|
||||
}
|
||||
if req.PersistentKeepalive != 0 {
|
||||
peer.PersistentKeepalive = req.PersistentKeepalive
|
||||
}
|
||||
if req.DNSStr != "" {
|
||||
peer.DNSStr = req.DNSStr
|
||||
}
|
||||
if req.Mtu != 0 {
|
||||
peer.Mtu = req.Mtu
|
||||
}
|
||||
|
||||
if err := s.s.CreatePeer(deviceName, peer); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
config, err := peer.GetConfigFile(device)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "text/plain", config)
|
||||
}
|
@@ -55,17 +55,18 @@ 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"` // must be an email address
|
||||
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"`
|
||||
SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
|
||||
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"` // must be an email address
|
||||
AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"`
|
||||
EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"`
|
||||
CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"`
|
||||
SelfProvisioningAllowed bool `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"`
|
||||
LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
|
||||
SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
|
||||
} `yaml:"core"`
|
||||
Database common.DatabaseConfig `yaml:"database"`
|
||||
Email common.MailConfig `yaml:"email"`
|
||||
@@ -96,15 +97,14 @@ func NewConfig() *Config {
|
||||
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.LDAP.LoginFilter = "(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))"
|
||||
cfg.LDAP.SyncFilter = "(&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))"
|
||||
|
||||
cfg.WG.DeviceNames = []string{"wg0"}
|
||||
cfg.WG.DefaultDeviceName = "wg0"
|
||||
@@ -112,6 +112,8 @@ func NewConfig() *Config {
|
||||
cfg.WG.ManageIPAddresses = true
|
||||
cfg.Email.Host = "127.0.0.1"
|
||||
cfg.Email.Port = 25
|
||||
cfg.Email.Encryption = common.MailEncryptionNone
|
||||
cfg.Email.AuthType = common.MailAuthPlain
|
||||
|
||||
// Load config from file and environment
|
||||
cfgFile, ok := os.LookupEnv("CONFIG_FILE")
|
||||
|
1531
internal/server/docs/docs.go
Normal file
1531
internal/server/docs/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,13 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
csrf "github.com/utrack/gin-csrf"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/authentication"
|
||||
"github.com/h44z/wg-portal/internal/users"
|
||||
"github.com/sirupsen/logrus"
|
||||
csrf "github.com/utrack/gin-csrf"
|
||||
)
|
||||
|
||||
func (s *Server) GetLogin(c *gin.Context) {
|
||||
@@ -54,65 +55,15 @@ func (s *Server) PostLogin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
userData, err := loginProvider.GetUserModel(&authentication.AuthContext{
|
||||
Username: email,
|
||||
})
|
||||
if err != nil {
|
||||
s.GetHandleError(c, http.StatusInternalServerError, "login error", err.Error())
|
||||
return
|
||||
}
|
||||
if err := s.CreateUser(users.User{
|
||||
Email: userData.Email,
|
||||
Source: users.UserSource(loginProvider.GetName()),
|
||||
IsAdmin: userData.IsAdmin,
|
||||
Firstname: userData.Firstname,
|
||||
Lastname: userData.Lastname,
|
||||
Phone: userData.Phone,
|
||||
}, s.wg.Cfg.GetDefaultDeviceName()); err != nil {
|
||||
s.GetHandleError(c, http.StatusInternalServerError, "login error", "failed to update user data")
|
||||
return
|
||||
}
|
||||
|
||||
user = s.users.GetUser(username)
|
||||
break
|
||||
}
|
||||
// Check all available auth backends
|
||||
user, err := s.checkAuthentication(username, password)
|
||||
if err != nil {
|
||||
s.GetHandleError(c, http.StatusInternalServerError, "login error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
if email == "" || loginProvider == nil || user == nil {
|
||||
if user == nil {
|
||||
c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail")
|
||||
return
|
||||
}
|
||||
@@ -153,3 +104,48 @@ func (s *Server) GetLogout(c *gin.Context) {
|
||||
}
|
||||
c.Redirect(http.StatusSeeOther, "/")
|
||||
}
|
||||
|
||||
func (s *Server) checkAuthentication(username, password string) (*users.User, error) {
|
||||
var user *users.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
|
||||
}
|
||||
|
||||
// Login succeeded
|
||||
user = s.users.GetUser(authEmail)
|
||||
if user != nil {
|
||||
break // user exists, nothing more to do...
|
||||
}
|
||||
|
||||
// create new user in the database (or reactivate him)
|
||||
userData, err := provider.GetUserModel(&authentication.AuthContext{
|
||||
Username: username,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get user model")
|
||||
}
|
||||
if err := s.CreateUser(users.User{
|
||||
Email: userData.Email,
|
||||
Source: users.UserSource(provider.GetName()),
|
||||
IsAdmin: userData.IsAdmin,
|
||||
Firstname: userData.Firstname,
|
||||
Lastname: userData.Lastname,
|
||||
Phone: userData.Phone,
|
||||
}, s.wg.Cfg.GetDefaultDeviceName()); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to update user data")
|
||||
}
|
||||
|
||||
user = s.users.GetUser(authEmail)
|
||||
break
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
@@ -265,6 +265,7 @@ func (s *Server) GetPeerConfigMail(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
// Apply mail template
|
||||
qrcodeFileName := "wireguard-qrcode.png"
|
||||
var tplBuff bytes.Buffer
|
||||
if err := s.mailTpl.Execute(&tplBuff, struct {
|
||||
Peer wireguard.Peer
|
||||
@@ -274,7 +275,7 @@ func (s *Server) GetPeerConfigMail(c *gin.Context) {
|
||||
}{
|
||||
Peer: peer,
|
||||
User: user,
|
||||
QrcodePngName: "wireguard-config.png",
|
||||
QrcodePngName: qrcodeFileName,
|
||||
PortalUrl: s.config.Core.ExternalUrl,
|
||||
}); err != nil {
|
||||
s.GetHandleError(c, http.StatusInternalServerError, "Template error", err.Error())
|
||||
@@ -289,7 +290,13 @@ func (s *Server) GetPeerConfigMail(c *gin.Context) {
|
||||
Data: bytes.NewReader(cfg),
|
||||
},
|
||||
{
|
||||
Name: "wireguard-config.png",
|
||||
Name: qrcodeFileName,
|
||||
ContentType: "image/png",
|
||||
Data: bytes.NewReader(png),
|
||||
Embedded: true,
|
||||
},
|
||||
{
|
||||
Name: qrcodeFileName,
|
||||
ContentType: "image/png",
|
||||
Data: bytes.NewReader(png),
|
||||
},
|
||||
|
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/users"
|
||||
csrf "github.com/utrack/gin-csrf"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -105,19 +104,6 @@ func (s *Server) PostAdminUsersEdit(c *gin.Context) {
|
||||
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{
|
||||
@@ -175,15 +161,7 @@ func (s *Server) PostAdminUsersCreate(c *gin.Context) {
|
||||
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 {
|
||||
if formUser.Password == "" {
|
||||
_ = s.updateFormInSession(c, formUser)
|
||||
SetFlashMessage(c, "invalid password", "danger")
|
||||
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create")
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/ldap"
|
||||
@@ -31,107 +32,33 @@ func (s *Server) SyncLdapWithUserDatabase() {
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// Update existing LDAP users
|
||||
s.updateLdapUsers(ldapUsers)
|
||||
|
||||
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 err = s.users.UpdateUser(user); err != nil {
|
||||
logrus.Errorf("failed to update ldap user %s in database: %v", user.Email, err)
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Disable missing LDAP users
|
||||
s.disableMissingLdapUsers(ldapUsers)
|
||||
}
|
||||
logrus.Info("ldap user synchronization stopped")
|
||||
}
|
||||
|
||||
func (s Server) UserChangedInLdap(user *users.User, ldapData *ldap.RawLdapData) bool {
|
||||
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] {
|
||||
if user.Email != strings.ToLower(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 user.Source != users.UserSourceLdap {
|
||||
return true
|
||||
}
|
||||
if ldapDeactivated != user.DeletedAt.Valid {
|
||||
|
||||
if user.DeletedAt.Valid {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -148,3 +75,88 @@ func (s Server) UserChangedInLdap(user *users.User, ldapData *ldap.RawLdapData)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Server) disableMissingLdapUsers(ldapUsers []ldap.RawLdapData) {
|
||||
// Disable missing LDAP users
|
||||
activeUsers := s.users.GetUsers()
|
||||
for i := range activeUsers {
|
||||
if activeUsers[i].Source != users.UserSourceLdap {
|
||||
continue
|
||||
}
|
||||
|
||||
existsInLDAP := false
|
||||
for j := range ldapUsers {
|
||||
if activeUsers[i].Email == strings.ToLower(ldapUsers[j].Attributes[s.config.LDAP.EmailAttribute]) {
|
||||
existsInLDAP = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if existsInLDAP {
|
||||
continue
|
||||
}
|
||||
|
||||
// disable all peers for the given user
|
||||
for _, peer := range s.peers.GetPeersByMail(activeUsers[i].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)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.users.DeleteUser(&activeUsers[i]); err != nil {
|
||||
logrus.Errorf("failed to delete deactivated user %s in database: %v", activeUsers[i].Email, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData) {
|
||||
for i := range ldapUsers {
|
||||
if ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute] == "" {
|
||||
logrus.Tracef("skipping sync of %s, empty email attribute", ldapUsers[i].DN)
|
||||
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)
|
||||
}
|
||||
|
||||
// re-enable LDAP user if the user was disabled
|
||||
if user.DeletedAt.Valid {
|
||||
// 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]) {
|
||||
logrus.Debugf("updating ldap user %s", user.Email)
|
||||
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 err = s.users.UpdateUser(user); err != nil {
|
||||
logrus.Errorf("failed to update ldap user %s in database: %v", user.Email, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,12 +2,25 @@ package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
wgportal "github.com/h44z/wg-portal"
|
||||
_ "github.com/h44z/wg-portal/internal/server/docs" // docs is generated by Swag CLI, you have to import it.
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"github.com/swaggo/gin-swagger/swaggerFiles"
|
||||
csrf "github.com/utrack/gin-csrf"
|
||||
)
|
||||
|
||||
func SetupRoutes(s *Server) {
|
||||
csrfMiddleware := csrf.Middleware(csrf.Options{
|
||||
Secret: s.config.Core.SessionSecret,
|
||||
ErrorFunc: func(c *gin.Context) {
|
||||
c.String(400, "CSRF token mismatch")
|
||||
c.Abort()
|
||||
},
|
||||
})
|
||||
|
||||
// Startpage
|
||||
s.server.GET("/", s.GetIndex)
|
||||
s.server.GET("/favicon.ico", func(c *gin.Context) {
|
||||
@@ -21,12 +34,14 @@ func SetupRoutes(s *Server) {
|
||||
|
||||
// Auth routes
|
||||
auth := s.server.Group("/auth")
|
||||
auth.Use(csrfMiddleware)
|
||||
auth.GET("/login", s.GetLogin)
|
||||
auth.POST("/login", s.PostLogin)
|
||||
auth.GET("/logout", s.GetLogout)
|
||||
|
||||
// Admin routes
|
||||
admin := s.server.Group("/admin")
|
||||
admin.Use(csrfMiddleware)
|
||||
admin.Use(s.RequireAuthentication("admin"))
|
||||
admin.GET("/", s.GetAdminIndex)
|
||||
admin.GET("/device/edit", s.GetAdminEditInterface)
|
||||
@@ -52,6 +67,7 @@ func SetupRoutes(s *Server) {
|
||||
|
||||
// User routes
|
||||
user := s.server.Group("/user")
|
||||
user.Use(csrfMiddleware)
|
||||
user.Use(s.RequireAuthentication("")) // empty scope = all logged in users
|
||||
user.GET("/qrcode", s.GetPeerQRCode)
|
||||
user.GET("/profile", s.GetUserIndex)
|
||||
@@ -60,6 +76,44 @@ func SetupRoutes(s *Server) {
|
||||
user.GET("/status", s.GetPeerStatus)
|
||||
}
|
||||
|
||||
func SetupApiRoutes(s *Server) {
|
||||
api := ApiServer{s: s}
|
||||
|
||||
// Admin authenticated routes
|
||||
apiV1Backend := s.server.Group("/api/v1/backend")
|
||||
apiV1Backend.Use(s.RequireApiAuthentication("admin"))
|
||||
|
||||
apiV1Backend.GET("/users", api.GetUsers)
|
||||
apiV1Backend.POST("/users", api.PostUser)
|
||||
apiV1Backend.GET("/user", api.GetUser)
|
||||
apiV1Backend.PUT("/user", api.PutUser)
|
||||
apiV1Backend.PATCH("/user", api.PatchUser)
|
||||
apiV1Backend.DELETE("/user", api.DeleteUser)
|
||||
|
||||
apiV1Backend.GET("/peers", api.GetPeers)
|
||||
apiV1Backend.POST("/peers", api.PostPeer)
|
||||
apiV1Backend.GET("/peer", api.GetPeer)
|
||||
apiV1Backend.PUT("/peer", api.PutPeer)
|
||||
apiV1Backend.PATCH("/peer", api.PatchPeer)
|
||||
apiV1Backend.DELETE("/peer", api.DeletePeer)
|
||||
|
||||
apiV1Backend.GET("/devices", api.GetDevices)
|
||||
apiV1Backend.GET("/device", api.GetDevice)
|
||||
apiV1Backend.PUT("/device", api.PutDevice)
|
||||
apiV1Backend.PATCH("/device", api.PatchDevice)
|
||||
|
||||
// Simple authenticated routes
|
||||
apiV1Deployment := s.server.Group("/api/v1/provisioning")
|
||||
apiV1Deployment.Use(s.RequireApiAuthentication(""))
|
||||
|
||||
apiV1Deployment.GET("/peers", api.GetPeerDeploymentInformation)
|
||||
apiV1Deployment.GET("/peer", api.GetPeerDeploymentConfig)
|
||||
apiV1Deployment.POST("/peers", api.PostPeerDeploymentConfig)
|
||||
|
||||
// Swagger doc/ui
|
||||
s.server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
|
||||
func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSessionData(c)
|
||||
@@ -78,7 +132,7 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// default case if some randome scope was set...
|
||||
// default case if some random scope was set...
|
||||
if scope != "" && !session.IsAdmin {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
@@ -90,3 +144,55 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RequireApiAuthentication(scope string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
username, password, hasAuth := c.Request.BasicAuth()
|
||||
if !hasAuth {
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate form input
|
||||
if strings.Trim(username, " ") == "" || strings.Trim(password, " ") == "" {
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check all available auth backends
|
||||
user, err := s.checkAuthentication(username, password)
|
||||
if err != nil {
|
||||
c.Abort()
|
||||
c.JSON(http.StatusInternalServerError, ApiError{Message: "login error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
if user == nil {
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check admin scope
|
||||
if scope == "admin" && !user.IsAdmin {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusForbidden, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// default case if some random scope was set...
|
||||
if scope != "" && !user.IsAdmin {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusForbidden, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Continue down the chain to handler etc
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
@@ -26,7 +26,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
ginlogrus "github.com/toorop/gin-logrus"
|
||||
csrf "github.com/utrack/gin-csrf"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -104,7 +103,7 @@ func (s *Server) Setup(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "database setup failed")
|
||||
}
|
||||
err = common.MigrateDatabase(s.db, Version)
|
||||
err = common.MigrateDatabase(s.db, DatabaseVersion)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "database migration failed")
|
||||
}
|
||||
@@ -118,13 +117,6 @@ func (s *Server) Setup(ctx context.Context) error {
|
||||
}
|
||||
s.server.Use(gin.Recovery())
|
||||
s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte(s.config.Core.SessionSecret))))
|
||||
s.server.Use(csrf.Middleware(csrf.Options{
|
||||
Secret: s.config.Core.SessionSecret,
|
||||
ErrorFunc: func(c *gin.Context) {
|
||||
c.String(400, "CSRF token mismatch")
|
||||
c.Abort()
|
||||
},
|
||||
}))
|
||||
s.server.SetFuncMap(template.FuncMap{
|
||||
"formatBytes": common.ByteCountSI,
|
||||
"urlEncode": url.QueryEscape,
|
||||
@@ -151,6 +143,7 @@ func (s *Server) Setup(ctx context.Context) error {
|
||||
|
||||
// Setup all routes
|
||||
SetupRoutes(s)
|
||||
SetupApiRoutes(s)
|
||||
|
||||
// Setup user database (also needed for database authentication)
|
||||
s.users, err = users.NewManager(s.db)
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/h44z/wg-portal/internal/wireguard"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -52,6 +53,7 @@ func (s *Server) PrepareNewPeer(device string) (wireguard.Peer, error) {
|
||||
peer.PersistentKeepalive = dev.DefaultPersistentKeepalive
|
||||
peer.AllowedIPsStr = dev.DefaultAllowedIPsStr
|
||||
peer.Mtu = dev.Mtu
|
||||
peer.DeviceName = device
|
||||
case wireguard.DeviceTypeClient:
|
||||
peer.UID = "newendpoint"
|
||||
}
|
||||
@@ -225,6 +227,15 @@ func (s *Server) CreateUser(user users.User, device string) error {
|
||||
return s.UpdateUser(user)
|
||||
}
|
||||
|
||||
// Hash user password (if set)
|
||||
if user.Password != "" {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to hash password")
|
||||
}
|
||||
user.Password = users.PrivateString(hashedPassword)
|
||||
}
|
||||
|
||||
// Create user in database
|
||||
if err := s.users.CreateUser(&user); err != nil {
|
||||
return errors.WithMessage(err, "failed to create user in manager")
|
||||
@@ -243,6 +254,17 @@ func (s *Server) UpdateUser(user users.User) error {
|
||||
|
||||
currentUser := s.users.GetUserUnscoped(user.Email)
|
||||
|
||||
// Hash user password (if set)
|
||||
if user.Password != "" {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to hash password")
|
||||
}
|
||||
user.Password = users.PrivateString(hashedPassword)
|
||||
} else {
|
||||
user.Password = currentUser.Password // keep current password
|
||||
}
|
||||
|
||||
// Update in database
|
||||
if err := s.users.UpdateUser(&user); err != nil {
|
||||
return errors.WithMessage(err, "failed to update user in manager")
|
||||
|
@@ -1,3 +1,4 @@
|
||||
package server
|
||||
|
||||
var Version = "1.0.6"
|
||||
var Version = "testbuild"
|
||||
var DatabaseVersion = "1.0.7"
|
||||
|
@@ -51,6 +51,8 @@ func (m Manager) UserExists(email string) bool {
|
||||
}
|
||||
|
||||
func (m Manager) GetUser(email string) *User {
|
||||
email = strings.ToLower(email)
|
||||
|
||||
user := User{}
|
||||
m.db.Where("email = ?", email).First(&user)
|
||||
|
||||
@@ -62,6 +64,8 @@ func (m Manager) GetUser(email string) *User {
|
||||
}
|
||||
|
||||
func (m Manager) GetUserUnscoped(email string) *User {
|
||||
email = strings.ToLower(email)
|
||||
|
||||
user := User{}
|
||||
m.db.Unscoped().Where("email = ?", email).First(&user)
|
||||
|
||||
@@ -93,6 +97,8 @@ func (m Manager) GetFilteredAndSortedUsersUnscoped(sortKey, sortDirection, searc
|
||||
}
|
||||
|
||||
func (m Manager) GetOrCreateUser(email string) (*User, error) {
|
||||
email = strings.ToLower(email)
|
||||
|
||||
user := User{}
|
||||
m.db.Where("email = ?", email).FirstOrInit(&user)
|
||||
|
||||
@@ -113,6 +119,8 @@ func (m Manager) GetOrCreateUser(email string) (*User, error) {
|
||||
}
|
||||
|
||||
func (m Manager) GetOrCreateUserUnscoped(email string) (*User, error) {
|
||||
email = strings.ToLower(email)
|
||||
|
||||
user := User{}
|
||||
m.db.Unscoped().Where("email = ?", email).FirstOrInit(&user)
|
||||
|
||||
@@ -133,6 +141,8 @@ func (m Manager) GetOrCreateUserUnscoped(email string) (*User, error) {
|
||||
}
|
||||
|
||||
func (m Manager) CreateUser(user *User) error {
|
||||
user.Email = strings.ToLower(user.Email)
|
||||
user.Source = UserSourceDatabase
|
||||
res := m.db.Create(user)
|
||||
if res.Error != nil {
|
||||
return errors.Wrapf(res.Error, "failed to create user %s", user.Email)
|
||||
@@ -142,6 +152,7 @@ func (m Manager) CreateUser(user *User) error {
|
||||
}
|
||||
|
||||
func (m Manager) UpdateUser(user *User) error {
|
||||
user.Email = strings.ToLower(user.Email)
|
||||
res := m.db.Save(user)
|
||||
if res.Error != nil {
|
||||
return errors.Wrapf(res.Error, "failed to update user %s", user.Email)
|
||||
@@ -151,6 +162,7 @@ func (m Manager) UpdateUser(user *User) error {
|
||||
}
|
||||
|
||||
func (m Manager) DeleteUser(user *User) error {
|
||||
user.Email = strings.ToLower(user.Email)
|
||||
res := m.db.Delete(user)
|
||||
if res.Error != nil {
|
||||
return errors.Wrapf(res.Error, "failed to update user %s", user.Email)
|
||||
@@ -200,7 +212,7 @@ func filterUsers(users []User, search string) []User {
|
||||
|
||||
filteredUsers := make([]User, 0, len(users))
|
||||
for i := range users {
|
||||
if strings.Contains(users[i].Email, search) ||
|
||||
if strings.Contains(users[i].Email, strings.ToLower(search)) ||
|
||||
strings.Contains(users[i].Firstname, search) ||
|
||||
strings.Contains(users[i].Lastname, search) ||
|
||||
strings.Contains(string(users[i].Source), search) ||
|
||||
|
@@ -14,6 +14,16 @@ const (
|
||||
UserSourceOIDC UserSource = "oidc" // open id connect, TODO: implement
|
||||
)
|
||||
|
||||
type PrivateString string
|
||||
|
||||
func (PrivateString) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`""`), nil
|
||||
}
|
||||
|
||||
func (PrivateString) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -27,10 +37,10 @@ type User struct {
|
||||
Phone string `form:"phone" binding:"omitempty"`
|
||||
|
||||
// optional, integrated password authentication
|
||||
Password string `form:"password" binding:"omitempty"`
|
||||
Password PrivateString `form:"password" binding:"omitempty"`
|
||||
|
||||
// database internal fields
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:",omitempty"`
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
@@ -4,9 +4,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/milosgajdos/tenus"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const DefaultMTU = 1420
|
||||
|
@@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/h44z/wg-portal/internal/common"
|
||||
@@ -64,21 +63,21 @@ func init() {
|
||||
//
|
||||
|
||||
type Peer struct {
|
||||
Peer *wgtypes.Peer `gorm:"-"` // WireGuard peer
|
||||
Device *Device `gorm:"foreignKey:DeviceName" binding:"-"` // linked WireGuard device
|
||||
Config string `gorm:"-"`
|
||||
Peer *wgtypes.Peer `gorm:"-" json:"-"` // WireGuard peer
|
||||
Device *Device `gorm:"foreignKey:DeviceName" binding:"-" json:"-"` // linked WireGuard device
|
||||
Config string `gorm:"-" json:"-"`
|
||||
|
||||
UID string `form:"uid" binding:"required,alphanum"` // uid for html identification
|
||||
UID string `form:"uid" binding:"required,alphanum" json:"-"` // uid for html identification
|
||||
DeviceName string `gorm:"index" form:"device" binding:"required"`
|
||||
DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server"`
|
||||
DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server" json:"-"`
|
||||
Identifier string `form:"identifier" binding:"required,max=64"` // Identifier AND Email make a WireGuard peer unique
|
||||
Email string `gorm:"index" form:"mail" binding:"required,email"`
|
||||
IgnoreGlobalSettings bool `form:"ignoreglobalsettings"`
|
||||
|
||||
IsOnline bool `gorm:"-"`
|
||||
IsNew bool `gorm:"-"`
|
||||
LastHandshake string `gorm:"-"`
|
||||
LastHandshakeTime string `gorm:"-"`
|
||||
IsOnline bool `gorm:"-" json:"-"`
|
||||
IsNew bool `gorm:"-" json:"-"`
|
||||
LastHandshake string `gorm:"-" json:"-"`
|
||||
LastHandshakeTime string `gorm:"-" json:"-"`
|
||||
|
||||
// Core WireGuard Settings
|
||||
PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` // the public key of the peer itself
|
||||
@@ -94,7 +93,7 @@ type Peer struct {
|
||||
// Global Device Settings (can be ignored, only make sense if device is in server mode)
|
||||
Mtu int `form:"mtu" binding:"gte=0,lte=1500"`
|
||||
|
||||
DeactivatedAt *time.Time
|
||||
DeactivatedAt *time.Time `json:",omitempty"`
|
||||
CreatedBy string
|
||||
UpdatedBy string
|
||||
CreatedAt time.Time
|
||||
@@ -125,7 +124,7 @@ func (p Peer) GetAllowedIPs() []string {
|
||||
return common.ParseStringList(p.AllowedIPsStr)
|
||||
}
|
||||
|
||||
func (p Peer) GetConfig(_ *Device) wgtypes.PeerConfig {
|
||||
func (p Peer) GetConfig(dev *Device) wgtypes.PeerConfig {
|
||||
publicKey, _ := wgtypes.ParseKey(p.PublicKey)
|
||||
|
||||
var presharedKey *wgtypes.Key
|
||||
@@ -135,7 +134,7 @@ func (p Peer) GetConfig(_ *Device) wgtypes.PeerConfig {
|
||||
}
|
||||
|
||||
var endpoint *net.UDPAddr
|
||||
if p.Endpoint != "" {
|
||||
if p.Endpoint != "" && dev.Type == DeviceTypeClient {
|
||||
addr, err := net.ResolveUDPAddr("udp", p.Endpoint)
|
||||
if err == nil {
|
||||
endpoint = addr
|
||||
@@ -148,12 +147,18 @@ func (p Peer) GetConfig(_ *Device) wgtypes.PeerConfig {
|
||||
keepAlive = &keepAliveDuration
|
||||
}
|
||||
|
||||
peerAllowedIPs := p.GetAllowedIPs()
|
||||
allowedIPs := make([]net.IPNet, len(peerAllowedIPs))
|
||||
for i, ip := range peerAllowedIPs {
|
||||
allowedIPs := make([]net.IPNet, 0)
|
||||
var peerAllowedIPs []string
|
||||
switch dev.Type {
|
||||
case DeviceTypeClient:
|
||||
peerAllowedIPs = p.GetAllowedIPs()
|
||||
case DeviceTypeServer:
|
||||
peerAllowedIPs = p.GetIPAddresses()
|
||||
}
|
||||
for _, ip := range peerAllowedIPs {
|
||||
_, ipNet, err := net.ParseCIDR(ip)
|
||||
if err == nil {
|
||||
allowedIPs[i] = *ipNet
|
||||
allowedIPs = append(allowedIPs, *ipNet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,12 +192,21 @@ func (p Peer) GetConfigFile(device Device) ([]byte, error) {
|
||||
|
||||
func (p Peer) GetQRCode() ([]byte, error) {
|
||||
png, err := qrcode.Encode(p.Config, qrcode.Medium, 250)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to create qrcode")
|
||||
if err == nil {
|
||||
return png, nil
|
||||
}
|
||||
|
||||
if err.Error() != "content too long to encode" {
|
||||
logrus.Errorf("failed to create qrcode: %v", err)
|
||||
return nil, errors.Wrap(err, "failed to encode qrcode")
|
||||
}
|
||||
|
||||
png, err = qrcode.Encode(p.Config, qrcode.Low, 250)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to create qrcode: %v", err)
|
||||
return nil, errors.Wrap(err, "failed to encode qrcode")
|
||||
}
|
||||
|
||||
return png, nil
|
||||
}
|
||||
|
||||
@@ -221,7 +235,7 @@ const (
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
Interface *wgtypes.Device `gorm:"-"`
|
||||
Interface *wgtypes.Device `gorm:"-" json:"-"`
|
||||
|
||||
Type DeviceType `form:"devicetype" binding:"required,oneof=client server"`
|
||||
DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"`
|
||||
@@ -605,7 +619,7 @@ func (m *PeerManager) GetFilteredAndSortedPeers(device, sortKey, sortDirection,
|
||||
m.populatePeerData(&peers[i])
|
||||
|
||||
if search == "" ||
|
||||
strings.Contains(peers[i].Email, search) ||
|
||||
strings.Contains(peers[i].Email, strings.ToLower(search)) ||
|
||||
strings.Contains(peers[i].Identifier, search) ||
|
||||
strings.Contains(peers[i].PublicKey, search) {
|
||||
filteredPeers = append(filteredPeers, peers[i])
|
||||
@@ -618,6 +632,7 @@ func (m *PeerManager) GetFilteredAndSortedPeers(device, sortKey, sortDirection,
|
||||
}
|
||||
|
||||
func (m *PeerManager) GetSortedPeersForEmail(sortKey, sortDirection, email string) []Peer {
|
||||
email = strings.ToLower(email)
|
||||
peers := make([]Peer, 0)
|
||||
m.db.Where("email = ?", email).Find(&peers)
|
||||
|
||||
@@ -686,6 +701,7 @@ func (m *PeerManager) GetPeerByKey(publicKey string) Peer {
|
||||
}
|
||||
|
||||
func (m *PeerManager) GetPeersByMail(mail string) []Peer {
|
||||
mail = strings.ToLower(mail)
|
||||
var peers []Peer
|
||||
m.db.Where("email = ?", mail).Find(&peers)
|
||||
for i := range peers {
|
||||
@@ -701,6 +717,7 @@ func (m *PeerManager) CreatePeer(peer Peer) error {
|
||||
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
|
||||
peer.UpdatedAt = time.Now()
|
||||
peer.CreatedAt = time.Now()
|
||||
peer.Email = strings.ToLower(peer.Email)
|
||||
|
||||
res := m.db.Create(&peer)
|
||||
if res.Error != nil {
|
||||
@@ -713,6 +730,7 @@ func (m *PeerManager) CreatePeer(peer Peer) error {
|
||||
|
||||
func (m *PeerManager) UpdatePeer(peer Peer) error {
|
||||
peer.UpdatedAt = time.Now()
|
||||
peer.Email = strings.ToLower(peer.Email)
|
||||
|
||||
res := m.db.Save(&peer)
|
||||
if res.Error != nil {
|
||||
|
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
goss -g /app/goss/wgportal/goss.yaml validate --format json_oneline
|
||||
|
||||
exit 0
|
@@ -1,3 +0,0 @@
|
||||
process:
|
||||
wgportal:
|
||||
running: true
|
@@ -1,3 +0,0 @@
|
||||
process:
|
||||
wgportal:
|
||||
running: true
|
Reference in New Issue
Block a user