many more improvements and cleanup

This commit is contained in:
Christoph Haas 2023-07-24 23:26:22 +02:00
parent 2a5b4fe31d
commit 984818c393
19 changed files with 179 additions and 83 deletions

View File

@ -2,7 +2,7 @@
<configuration default="false" name="wg-portal-migrate" type="GoApplicationRunConfiguration" factoryName="Go Application"> <configuration default="false" name="wg-portal-migrate" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="wg-portal" /> <module name="wg-portal" />
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<parameters value="-migrateFrom=test_source.db" /> <parameters value="-migrateFrom=wg_portal.db" />
<envs> <envs>
<env name="SESSION_SECRET" value="extremlybad" /> <env name="SESSION_SECRET" value="extremlybad" />
<env name="LOG_LEVEL" value="trace" /> <env name="LOG_LEVEL" value="trace" />

View File

@ -4,6 +4,7 @@ import {userStore} from "@/stores/users";
import {computed, ref, watch} from "vue"; import {computed, ref, watch} from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import {freshUser} from "@/helpers/models";
const { t } = useI18n() const { t } = useI18n()
@ -30,34 +31,14 @@ const title = computed(() => {
return t("users.new") return t("users.new")
}) })
const formData = ref(freshFormData()) const formData = ref(freshUser())
function freshFormData() {
return {
Identifier: "",
Email: "",
Source: "db",
IsAdmin: false,
Firstname: "",
Lastname: "",
Phone: "",
Department: "",
Notes: "",
Password: "",
Disabled: false,
}
}
// functions // functions
watch(() => props.visible, async (newValue, oldValue) => { watch(() => props.visible, async (newValue, oldValue) => {
if (oldValue === false && newValue === true) { // if modal is shown if (oldValue === false && newValue === true) { // if modal is shown
if (!selectedUser.value) { if (!selectedUser.value) {
formData.value = freshFormData() formData.value = freshUser()
} else { // fill existing userdata } else { // fill existing userdata
formData.value.Identifier = selectedUser.value.Identifier formData.value.Identifier = selectedUser.value.Identifier
formData.value.Email = selectedUser.value.Email formData.value.Email = selectedUser.value.Email
@ -76,7 +57,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
) )
function close() { function close() {
formData.value = freshFormData() formData.value = freshUser()
emit('close') emit('close')
} }
@ -115,7 +96,7 @@ async function del() {
<template> <template>
<Modal :title="title" :visible="visible" @close="close"> <Modal :title="title" :visible="visible" @close="close">
<template #default> <template #default>
<fieldset> <fieldset v-if="formData.Source==='db'">
<legend class="mt-4">General</legend> <legend class="mt-4">General</legend>
<div v-if="props.userId==='#NEW#'" class="form-group"> <div v-if="props.userId==='#NEW#'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.useredit.identifier') }}</label> <label class="form-label mt-4">{{ $t('modals.useredit.identifier') }}</label>
@ -131,7 +112,7 @@ async function del() {
<small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">Leave this field blank to keep current password.</small> <small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">Leave this field blank to keep current password.</small>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset v-if="formData.Source==='db'">
<legend class="mt-4">User Information</legend> <legend class="mt-4">User Information</legend>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.useredit.email') }}</label> <label class="form-label mt-4">{{ $t('modals.useredit.email') }}</label>
@ -169,9 +150,13 @@ async function del() {
<legend class="mt-4">State</legend> <legend class="mt-4">State</legend>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input v-model="formData.Disabled" class="form-check-input" type="checkbox"> <input v-model="formData.Disabled" class="form-check-input" type="checkbox">
<label class="form-check-label" >Disabled</label> <label class="form-check-label" >Disabled (no WireGuard connection and no login possible)</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input v-model="formData.Locked" class="form-check-input" type="checkbox">
<label class="form-check-label" >Locked (no login possible, WireGuard connections still work)</label>
</div>
<div class="form-check form-switch" v-if="formData.Source==='db'">
<input v-model="formData.IsAdmin" checked="" class="form-check-input" type="checkbox"> <input v-model="formData.IsAdmin" checked="" class="form-check-input" type="checkbox">
<label class="form-check-label">Is Admin</label> <label class="form-check-label">Is Admin</label>
</div> </div>
@ -180,7 +165,7 @@ async function del() {
</template> </template>
<template #footer> <template #footer>
<div class="flex-fill text-start"> <div class="flex-fill text-start">
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">Delete</button> <button v-if="props.userId!=='#NEW#'&&formData.Source==='db'" class="btn btn-danger me-1" type="button" @click.prevent="del">Delete</button>
</div> </div>
<button class="btn btn-primary me-1" type="button" @click.prevent="save">Save</button> <button class="btn btn-primary me-1" type="button" @click.prevent="save">Save</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">Discard</button> <button class="btn btn-secondary" type="button" @click.prevent="close">Discard</button>

View File

@ -66,7 +66,7 @@ function close() {
<div id="user" class="tab-pane fade active show"> <div id="user" class="tab-pane fade active show">
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item"> <li class="list-group-item">
User Information: <h4>User Information:</h4>
<table class="table table-sm table-borderless device-status-table"> <table class="table table-sm table-borderless device-status-table">
<tbody> <tbody>
<tr> <tr>
@ -89,11 +89,19 @@ function close() {
<td>{{ $t('users.label.department') }}:</td> <td>{{ $t('users.label.department') }}:</td>
<td>{{selectedUser.Department}}</td> <td>{{selectedUser.Department}}</td>
</tr> </tr>
<tr v-if="selectedUser.Disabled">
<td>{{ $t('users.label.disabled') }}:</td>
<td>{{selectedUser.DisabledReason}}</td>
</tr>
<tr v-if="selectedUser.Locked">
<td>{{ $t('users.label.locked') }}:</td>
<td>{{selectedUser.LockedReason}}</td>
</tr>
</tbody> </tbody>
</table> </table>
</li> </li>
<li class="list-group-item"> <li class="list-group-item" v-if="selectedUser.Notes">
Notes: <h4>Notes:</h4>
<table class="table table-sm table-borderless device-status-table"> <table class="table table-sm table-borderless device-status-table">
<tbody> <tbody>
<tr><td>{{selectedUser.Notes}}</td></tr> <tr><td>{{selectedUser.Notes}}</td></tr>

View File

@ -122,6 +122,29 @@ export function freshPeer() {
} }
} }
export function freshUser() {
return {
Identifier: "",
Email: "",
Source: "db",
IsAdmin: false,
Firstname: "",
Lastname: "",
Phone: "",
Department: "",
Notes: "",
Password: "",
Disabled: false,
DisabledReason: "",
Locked: false,
LockedReason: ""
}
}
export function freshStats() { export function freshStats() {
return { return {
IsConnected: false, IsConnected: false,

View File

@ -15,18 +15,6 @@ const viewedUserId = ref("")
onMounted(() => { onMounted(() => {
users.LoadUsers() users.LoadUsers()
}) })
function editUser(user) {
if(user.Source === 'db') {
editUserId.value = user.Identifier
} else {
notify({
title: "Forbidden",
text: "You can not edit this user!",
type: 'error',
})
}
}
</script> </script>
<template> <template>
@ -62,6 +50,7 @@ function editUser(user) {
<th scope="col"> <th scope="col">
<input id="flexCheckDefault" class="form-check-input" title="Select all" type="checkbox" value=""> <input id="flexCheckDefault" class="form-check-input" title="Select all" type="checkbox" value="">
</th><!-- select --> </th><!-- select -->
<th scope="col"></th><!-- status -->
<th scope="col">{{ $t('user.id') }}</th> <th scope="col">{{ $t('user.id') }}</th>
<th scope="col">{{ $t('user.email') }}</th> <th scope="col">{{ $t('user.email') }}</th>
<th scope="col">{{ $t('user.firstname') }}</th> <th scope="col">{{ $t('user.firstname') }}</th>
@ -77,6 +66,10 @@ function editUser(user) {
<th scope="row"> <th scope="row">
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value=""> <input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
</th> </th>
<td class="text-center">
<span v-if="user.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="user.DisabledReason"></i></span>
<span v-if="user.Locked" class="text-danger"><i class="fas fa-lock" :title="user.LockedReason"></i></span>
</td>
<td>{{user.Identifier}}</td> <td>{{user.Identifier}}</td>
<td>{{user.Email}}</td> <td>{{user.Email}}</td>
<td>{{user.Firstname}}</td> <td>{{user.Firstname}}</td>
@ -89,7 +82,7 @@ function editUser(user) {
</td> </td>
<td class="text-center"> <td class="text-center">
<a href="#" title="Show user" @click.prevent="viewedUserId=user.Identifier"><i class="fas fa-eye me-2"></i></a> <a href="#" title="Show user" @click.prevent="viewedUserId=user.Identifier"><i class="fas fa-eye me-2"></i></a>
<a :class="{disabled:user.Source!=='db'}" href="#" title="Edit user" @click.prevent="editUser(user)"><i class="fas fa-cog me-2"></i></a> <a href="#" title="Edit user" @click.prevent="editUserId=user.Identifier"><i class="fas fa-cog me-2"></i></a>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -263,8 +263,9 @@ func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.I
} }
func (r *SqlRepo) SaveInterface(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.Interface) (*domain.Interface, error)) error { func (r *SqlRepo) SaveInterface(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.Interface) (*domain.Interface, error)) error {
userInfo := domain.GetUserInfo(ctx)
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
in, err := r.getOrCreateInterface(tx, id) in, err := r.getOrCreateInterface(userInfo, tx, id)
if err != nil { if err != nil {
return err // return any error will roll back return err // return any error will roll back
} }
@ -274,7 +275,7 @@ func (r *SqlRepo) SaveInterface(ctx context.Context, id domain.InterfaceIdentifi
return err return err
} }
err = r.upsertInterface(tx, in) err = r.upsertInterface(userInfo, tx, in)
if err != nil { if err != nil {
return err return err
} }
@ -289,12 +290,14 @@ func (r *SqlRepo) SaveInterface(ctx context.Context, id domain.InterfaceIdentifi
return nil return nil
} }
func (r *SqlRepo) getOrCreateInterface(tx *gorm.DB, id domain.InterfaceIdentifier) (*domain.Interface, error) { func (r *SqlRepo) getOrCreateInterface(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.InterfaceIdentifier) (*domain.Interface, error) {
var in domain.Interface var in domain.Interface
// interfaceDefaults will be applied to newly created interface records // interfaceDefaults will be applied to newly created interface records
interfaceDefaults := domain.Interface{ interfaceDefaults := domain.Interface{
BaseModel: domain.BaseModel{ BaseModel: domain.BaseModel{
CreatedBy: ui.UserId(),
UpdatedBy: ui.UserId(),
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
@ -309,7 +312,10 @@ func (r *SqlRepo) getOrCreateInterface(tx *gorm.DB, id domain.InterfaceIdentifie
return &in, nil return &in, nil
} }
func (r *SqlRepo) upsertInterface(tx *gorm.DB, in *domain.Interface) error { func (r *SqlRepo) upsertInterface(ui *domain.ContextUserInfo, tx *gorm.DB, in *domain.Interface) error {
in.UpdatedBy = ui.UserId()
in.UpdatedAt = time.Now()
err := tx.Save(in).Error err := tx.Save(in).Error
if err != nil { if err != nil {
return err return err
@ -439,8 +445,9 @@ func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, s
} }
func (r *SqlRepo) SavePeer(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.Peer) (*domain.Peer, error)) error { func (r *SqlRepo) SavePeer(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.Peer) (*domain.Peer, error)) error {
userInfo := domain.GetUserInfo(ctx)
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
peer, err := r.getOrCreatePeer(tx, id) peer, err := r.getOrCreatePeer(userInfo, tx, id)
if err != nil { if err != nil {
return err // return any error will roll back return err // return any error will roll back
} }
@ -450,7 +457,7 @@ func (r *SqlRepo) SavePeer(ctx context.Context, id domain.PeerIdentifier, update
return err return err
} }
err = r.upsertPeer(tx, peer) err = r.upsertPeer(userInfo, tx, peer)
if err != nil { if err != nil {
return err return err
} }
@ -465,12 +472,14 @@ func (r *SqlRepo) SavePeer(ctx context.Context, id domain.PeerIdentifier, update
return nil return nil
} }
func (r *SqlRepo) getOrCreatePeer(tx *gorm.DB, id domain.PeerIdentifier) (*domain.Peer, error) { func (r *SqlRepo) getOrCreatePeer(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.PeerIdentifier) (*domain.Peer, error) {
var peer domain.Peer var peer domain.Peer
// interfaceDefaults will be applied to newly created interface records // interfaceDefaults will be applied to newly created interface records
interfaceDefaults := domain.Peer{ interfaceDefaults := domain.Peer{
BaseModel: domain.BaseModel{ BaseModel: domain.BaseModel{
CreatedBy: ui.UserId(),
UpdatedBy: ui.UserId(),
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
@ -485,7 +494,10 @@ func (r *SqlRepo) getOrCreatePeer(tx *gorm.DB, id domain.PeerIdentifier) (*domai
return &peer, nil return &peer, nil
} }
func (r *SqlRepo) upsertPeer(tx *gorm.DB, peer *domain.Peer) error { func (r *SqlRepo) upsertPeer(ui *domain.ContextUserInfo, tx *gorm.DB, peer *domain.Peer) error {
peer.UpdatedBy = ui.UserId()
peer.UpdatedAt = time.Now()
err := tx.Save(peer).Error err := tx.Save(peer).Error
if err != nil { if err != nil {
return err return err
@ -626,7 +638,7 @@ func (r *SqlRepo) SaveUser(ctx context.Context, id domain.UserIdentifier, update
userInfo := domain.GetUserInfo(ctx) userInfo := domain.GetUserInfo(ctx)
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
user, err := r.getOrCreateUser(string(userInfo.Id), tx, id) user, err := r.getOrCreateUser(userInfo, tx, id)
if err != nil { if err != nil {
return err // return any error will roll back return err // return any error will roll back
} }
@ -636,10 +648,7 @@ func (r *SqlRepo) SaveUser(ctx context.Context, id domain.UserIdentifier, update
return err return err
} }
user.UpdatedAt = time.Now() err = r.upsertUser(userInfo, tx, user)
user.UpdatedBy = string(userInfo.Id)
err = r.upsertUser(tx, user)
if err != nil { if err != nil {
return err return err
} }
@ -663,14 +672,14 @@ func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) erro
return nil return nil
} }
func (r *SqlRepo) getOrCreateUser(creator string, tx *gorm.DB, id domain.UserIdentifier) (*domain.User, error) { func (r *SqlRepo) getOrCreateUser(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.UserIdentifier) (*domain.User, error) {
var user domain.User var user domain.User
// userDefaults will be applied to newly created user records // userDefaults will be applied to newly created user records
userDefaults := domain.User{ userDefaults := domain.User{
BaseModel: domain.BaseModel{ BaseModel: domain.BaseModel{
CreatedBy: creator, CreatedBy: ui.UserId(),
UpdatedBy: creator, UpdatedBy: ui.UserId(),
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
@ -687,7 +696,10 @@ func (r *SqlRepo) getOrCreateUser(creator string, tx *gorm.DB, id domain.UserIde
return &user, nil return &user, nil
} }
func (r *SqlRepo) upsertUser(tx *gorm.DB, user *domain.User) error { func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *domain.User) error {
user.UpdatedBy = ui.UserId()
user.UpdatedAt = time.Now()
err := tx.Save(user).Error err := tx.Save(user).Error
if err != nil { if err != nil {
return err return err

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,7 +14,7 @@
let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal"; let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal";
</script> </script>
<script src="/api/v0/config/frontend.js"></script> <script src="/api/v0/config/frontend.js"></script>
<script type="module" crossorigin src="/app/assets/index-4f7c99b3.js"></script> <script type="module" crossorigin src="/app/assets/index-96214b1b.js"></script>
<link rel="stylesheet" href="/app/assets/index-7144f109.css"> <link rel="stylesheet" href="/app/assets/index-7144f109.css">
</head> </head>
<body class="d-flex flex-column min-vh-100"> <body class="d-flex flex-column min-vh-100">

View File

@ -22,6 +22,8 @@ type User struct {
Password string `json:"Password,omitempty"` Password string `json:"Password,omitempty"`
Disabled bool `json:"Disabled"` // if this field is set, the user is disabled Disabled bool `json:"Disabled"` // if this field is set, the user is disabled
DisabledReason string `json:"DisabledReason"` // the reason why the user has been disabled DisabledReason string `json:"DisabledReason"` // the reason why the user has been disabled
Locked bool `json:"Locked"` // if this field is set, the user is locked
LockedReason string `json:"LockedReason"` // the reason why the user has been locked
// Calculated // Calculated
@ -43,6 +45,8 @@ func NewUser(src *domain.User) *User {
Password: "", // never fill password Password: "", // never fill password
Disabled: src.IsDisabled(), Disabled: src.IsDisabled(),
DisabledReason: src.DisabledReason, DisabledReason: src.DisabledReason,
Locked: src.IsLocked(),
LockedReason: src.LockedReason,
PeerCount: src.LinkedPeerCount, PeerCount: src.LinkedPeerCount,
} }
@ -73,6 +77,8 @@ func NewDomainUser(src *User) *domain.User {
Password: domain.PrivateString(src.Password), Password: domain.PrivateString(src.Password),
Disabled: nil, // set below Disabled: nil, // set below
DisabledReason: src.DisabledReason, DisabledReason: src.DisabledReason,
Locked: nil, // set below
LockedReason: src.LockedReason,
LinkedPeerCount: src.PeerCount, LinkedPeerCount: src.PeerCount,
} }
@ -80,5 +86,9 @@ func NewDomainUser(src *User) *domain.User {
res.Disabled = &now res.Disabled = &now
} }
if src.Locked {
res.Locked = &now
}
return res return res
} }

View File

@ -159,6 +159,10 @@ func (a *Authenticator) IsUserValid(ctx context.Context, id domain.UserIdentifie
return false return false
} }
if user.IsLocked() {
return false
}
return true return true
} }
@ -193,6 +197,9 @@ func (a *Authenticator) passwordAuthentication(ctx context.Context, identifier d
userInDatabase = true userInDatabase = true
userSource = existingUser.Source userSource = existingUser.Source
} }
if userInDatabase && (existingUser.IsLocked() || existingUser.IsDisabled()) {
return nil, errors.New("user is locked")
}
if !userInDatabase || userSource == domain.UserSourceLdap { if !userInDatabase || userSource == domain.UserSourceLdap {
// search user in ldap if registration is enabled // search user in ldap if registration is enabled
@ -313,6 +320,10 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
return nil, fmt.Errorf("unable to process user information: %w", err) return nil, fmt.Errorf("unable to process user information: %w", err)
} }
if user.IsLocked() || user.IsDisabled() {
return nil, errors.New("user is locked")
}
a.bus.Publish(app.TopicAuthLogin, user.Identifier) a.bus.Publish(app.TopicAuthLogin, user.Identifier)
return user, nil return user, nil

View File

@ -62,6 +62,8 @@ func migrateFromV1(cfg *config.Config, db *gorm.DB, source, typ string) error {
return fmt.Errorf("peer migration failed: %w", err) return fmt.Errorf("peer migration failed: %w", err)
} }
logrus.Infof("Migrated V1 database with version %s, please restart WireGuard Portal", lastVersion.Version)
return nil return nil
} }
@ -121,6 +123,8 @@ func migrateV1Users(oldDb, newDb *gorm.DB) error {
if err := newDb.Save(&newUser).Error; err != nil { if err := newDb.Save(&newUser).Error; err != nil {
return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err) return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err)
} }
logrus.Debugf(" - User %s migrated", newUser.Identifier)
} }
return nil return nil
@ -213,6 +217,8 @@ func migrateV1Interfaces(oldDb, newDb *gorm.DB) error {
if err := newDb.Save(&newInterface).Error; err != nil { if err := newDb.Save(&newInterface).Error; err != nil {
return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err) return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err)
} }
logrus.Debugf(" - Interface %s migrated", newInterface.Identifier)
} }
return nil return nil
@ -302,13 +308,15 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
ProviderName: "", ProviderName: "",
IsAdmin: false, IsAdmin: false,
Locked: &now, Locked: &now,
LockedReason: "migration dummy user", LockedReason: domain.DisabledReasonMigrationDummy,
Notes: "created by migration from v1", Notes: "created by migration from v1",
} }
if err := newDb.Save(&user).Error; err != nil { if err := newDb.Save(&user).Error; err != nil {
return fmt.Errorf("failed to migrate dummy user %s: %w", oldPeer.Email, err) return fmt.Errorf("failed to migrate dummy user %s: %w", oldPeer.Email, err)
} }
logrus.Debugf(" - Dummy User %s migrated", user.Identifier)
} }
newPeer := domain.Peer{ newPeer := domain.Peer{
BaseModel: domain.BaseModel{ BaseModel: domain.BaseModel{
@ -379,6 +387,8 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
if err := newDb.Save(&newPeer).Error; err != nil { if err := newDb.Save(&newPeer).Error; err != nil {
return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err) return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err)
} }
logrus.Debugf(" - Peer %s migrated", newPeer.Identifier)
} }
return nil return nil

View File

@ -208,7 +208,7 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
return fmt.Errorf("insufficient permissions") return fmt.Errorf("insufficient permissions")
} }
if err := old.EditAllowed(); err != nil { if err := old.EditAllowed(new); err != nil {
return fmt.Errorf("no access: %w", err) return fmt.Errorf("no access: %w", err)
} }
@ -224,6 +224,10 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
return fmt.Errorf("cannot disable own user") return fmt.Errorf("cannot disable own user")
} }
if currentUser.Id == old.Identifier && new.IsLocked() {
return fmt.Errorf("cannot lock own user")
}
if old.Source != new.Source { if old.Source != new.Source {
return fmt.Errorf("cannot change user source") return fmt.Errorf("cannot change user source")
} }
@ -264,7 +268,7 @@ func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error {
return fmt.Errorf("insufficient permissions") return fmt.Errorf("insufficient permissions")
} }
if err := del.EditAllowed(); err != nil { if err := del.DeleteAllowed(); err != nil {
return fmt.Errorf("no access: %w", err) return fmt.Errorf("no access: %w", err)
} }

View File

@ -22,13 +22,14 @@ func (PrivateString) String() string {
} }
const ( const (
DisabledReasonExpired = "expired" DisabledReasonExpired = "expired"
DisabledReasonUserEdit = "user edit action" DisabledReasonUserEdit = "user edit action"
DisabledReasonUserCreate = "user create action" DisabledReasonUserCreate = "user create action"
DisabledReasonAdminEdit = "admin edit action" DisabledReasonAdminEdit = "admin edit action"
DisabledReasonAdminCreate = "admin create action" DisabledReasonAdminCreate = "admin create action"
DisabledReasonApiEdit = "api edit action" DisabledReasonApiEdit = "api edit action"
DisabledReasonApiCreate = "api create action" DisabledReasonApiCreate = "api create action"
DisabledReasonLdapMissing = "missing in ldap" DisabledReasonLdapMissing = "missing in ldap"
DisabledReasonUserMissing = "missing user" DisabledReasonUserMissing = "missing user"
DisabledReasonMigrationDummy = "migration dummy user"
) )

View File

@ -2,6 +2,7 @@ package domain
import ( import (
"context" "context"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -18,6 +19,14 @@ type ContextUserInfo struct {
IsAdmin bool IsAdmin bool
} }
func (u *ContextUserInfo) String() string {
return fmt.Sprintf("%s|%t", u.Id, u.IsAdmin)
}
func (u *ContextUserInfo) UserId() string {
return string(u.Id)
}
func DefaultContextUserInfo() *ContextUserInfo { func DefaultContextUserInfo() *ContextUserInfo {
return &ContextUserInfo{ return &ContextUserInfo{
Id: CtxUnknownUserId, Id: CtxUnknownUserId,

View File

@ -37,18 +37,25 @@ type User struct {
// optional, integrated password authentication // optional, integrated password authentication
Password PrivateString `form:"password" binding:"omitempty"` Password PrivateString `form:"password" binding:"omitempty"`
Disabled *time.Time `gorm:"index;column:disabled"` // if this field is set, the user is disabled Disabled *time.Time `gorm:"index;column:disabled"` // if this field is set, the user is disabled (WireGuard peers are disabled as well)
DisabledReason string // the reason why the user has been disabled DisabledReason string // the reason why the user has been disabled
Locked *time.Time `gorm:"index;column:locked"` // if this field is set, the user is locked and can no longer login Locked *time.Time `gorm:"index;column:locked"` // if this field is set, the user is locked and can no longer login (WireGuard peers still can connect)
LockedReason string // the reason why the user has been locked LockedReason string // the reason why the user has been locked
LinkedPeerCount int `gorm:"-"` LinkedPeerCount int `gorm:"-"`
} }
// IsDisabled returns true if the user is disabled. In such a case,
// no login is possible and WireGuard peers associated with the user are disabled.
func (u *User) IsDisabled() bool { func (u *User) IsDisabled() bool {
return u.Disabled != nil return u.Disabled != nil
} }
// IsLocked returns true if the user is locked. In such a case, no login is possible, WireGuard connections still work.
func (u *User) IsLocked() bool {
return u.Locked != nil
}
func (u *User) CanChangePassword() error { func (u *User) CanChangePassword() error {
if u.Source == UserSourceDatabase { if u.Source == UserSourceDatabase {
return nil return nil
@ -57,12 +64,35 @@ func (u *User) CanChangePassword() error {
return errors.New("password change only allowed for database source") return errors.New("password change only allowed for database source")
} }
func (u *User) EditAllowed() error { func (u *User) EditAllowed(new *User) error {
if u.Source == UserSourceDatabase { if u.Source == UserSourceDatabase {
return nil return nil
} }
return errors.New("edit only allowed for database source") // for users which are not database users, only the notes field and the disabled flag can be updated
updateOk := true
updateOk = updateOk && u.Identifier == new.Identifier
updateOk = updateOk && u.Source == new.Source
updateOk = updateOk && u.IsAdmin == new.IsAdmin
updateOk = updateOk && u.Email == new.Email
updateOk = updateOk && u.Firstname == new.Firstname
updateOk = updateOk && u.Lastname == new.Lastname
updateOk = updateOk && u.Phone == new.Phone
updateOk = updateOk && u.Department == new.Department
if !updateOk {
return errors.New("edit only allowed for database source")
}
return nil
}
func (u *User) DeleteAllowed() error {
if u.Source == UserSourceDatabase {
return nil
}
return errors.New("delete only allowed for database source")
} }
func (u *User) CheckPassword(password string) error { func (u *User) CheckPassword(password string) error {