Add NO_AUTH configuration option.

This option permit to use happyDNS without creating accounts.
This commit is contained in:
nemunaire 2021-01-03 22:40:47 +01:00
parent 4dc0c592d0
commit e80281c941
10 changed files with 190 additions and 125 deletions

View File

@ -211,7 +211,7 @@ type RequestResources struct {
Zone *happydns.Zone
}
func apiAuthHandler(f func(*config.Options, *RequestResources, io.Reader) Response) func(http.ResponseWriter, *http.Request, httprouter.Params) {
func apiOptionalAuthHandler(noauthcb func(*config.Options, *RequestResources, io.Reader) Response, authcb func(*config.Options, *RequestResources, io.Reader) Response) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
r.RemoteAddr = addr
@ -253,48 +253,61 @@ func apiAuthHandler(f func(*config.Options, *RequestResources, io.Reader) Respon
}
}
if sessionid == nil || len(sessionid) == 0 {
logResponse(r, APIErrorResponse{
err: fmt.Errorf("Authorization required"),
status: http.StatusUnauthorized,
}.WriteResponse(w))
} else if session, err := storage.MainStore.GetSession(sessionid); err != nil {
logResponse(r, APIErrorResponse{
err: err,
status: http.StatusUnauthorized,
cookies: []*http.Cookie{&http.Cookie{
Name: "happydns_session",
Value: "",
Path: opts.BaseURL + "/",
Expires: time.Unix(0, 0),
Secure: opts.DevProxy == "",
HttpOnly: true,
}},
}.WriteResponse(w))
} else if user, err := storage.MainStore.GetUser(session.IdUser); err != nil {
logResponse(r, APIErrorResponse{
err: err,
status: http.StatusUnauthorized,
cookies: []*http.Cookie{&http.Cookie{
Name: "happydns_session",
Value: "",
Path: opts.BaseURL + "/",
Expires: time.Unix(0, 0),
Secure: opts.DevProxy == "",
HttpOnly: true,
}},
}.WriteResponse(w))
} else {
req := &RequestResources{
Ps: ps,
Session: session,
User: user,
}
logResponse(r, f(opts, req, r.Body).WriteResponse(w))
var err error
req := &RequestResources{
Ps: ps,
}
if session.HasChanged() {
storage.MainStore.UpdateSession(session)
}
if sessionid == nil || len(sessionid) == 0 {
logResponse(r, noauthcb(opts, req, r.Body).WriteResponse(w))
} else if req.Session, err = storage.MainStore.GetSession(sessionid); err != nil {
logResponse(r, APIErrorResponse{
err: err,
status: http.StatusUnauthorized,
cookies: []*http.Cookie{&http.Cookie{
Name: "happydns_session",
Value: "",
Path: opts.BaseURL + "/",
Expires: time.Unix(0, 0),
Secure: opts.DevProxy == "",
HttpOnly: true,
}},
}.WriteResponse(w))
} else if req.User, err = storage.MainStore.GetUser(req.Session.IdUser); err != nil {
logResponse(r, APIErrorResponse{
err: err,
status: http.StatusUnauthorized,
cookies: []*http.Cookie{&http.Cookie{
Name: "happydns_session",
Value: "",
Path: opts.BaseURL + "/",
Expires: time.Unix(0, 0),
Secure: opts.DevProxy == "",
HttpOnly: true,
}},
}.WriteResponse(w))
} else if req.User.Email == NO_AUTH_ACCOUNT && !opts.NoAuth {
logResponse(r, noauthcb(opts, req, r.Body).WriteResponse(w))
} else {
logResponse(r, authcb(opts, req, r.Body).WriteResponse(w))
}
}
}
func apiAuthHandler(f func(*config.Options, *RequestResources, io.Reader) Response) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return apiOptionalAuthHandler(func(opts *config.Options, req *RequestResources, _ io.Reader) Response {
return APIErrorResponse{
err: fmt.Errorf("Authorization required"),
status: http.StatusUnauthorized,
}
}, func(opts *config.Options, req *RequestResources, r io.Reader) Response {
response := f(opts, req, r)
if req.Session.HasChanged() {
storage.MainStore.UpdateSession(req.Session)
}
return response
})
}

View File

@ -35,6 +35,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
@ -47,10 +48,12 @@ import (
"git.happydns.org/happydns/storage"
)
const NO_AUTH_ACCOUNT = "_no_auth"
var AuthFunc = checkAuth
func init() {
router.GET("/api/auth", apiAuthHandler(displayAuthToken))
router.GET("/api/auth", apiOptionalAuthHandler(displayNotAuthToken, displayAuthToken))
router.POST("/api/auth", ApiHandler(func(opts *config.Options, ps httprouter.Params, b io.Reader) Response {
return AuthFunc(opts, ps, b)
}))
@ -73,6 +76,17 @@ func currentUser(u *happydns.User) *DisplayUser {
}
}
func displayNotAuthToken(opts *config.Options, req *RequestResources, _ io.Reader) Response {
if opts.NoAuth {
return completeAuth(opts, NO_AUTH_ACCOUNT, NO_AUTH_ACCOUNT)
} else {
return APIErrorResponse{
err: fmt.Errorf("Authorization required"),
status: http.StatusUnauthorized,
}
}
}
func displayAuthToken(_ *config.Options, req *RequestResources, _ io.Reader) Response {
return APIResponse{
response: currentUser(req.User),

View File

@ -151,7 +151,7 @@ func registerUser(opts *config.Options, p httprouter.Params, body io.Reader) Res
if len(uu.Password) <= 7 {
return APIErrorResponse{
err: errors.New("The given email is invalid."),
err: errors.New("The given password is invalid."),
}
}

View File

@ -47,6 +47,7 @@ func (o *Options) declareFlags() {
flag.StringVar(&o.BaseURL, "baseurl", o.BaseURL, "URL prepended to each URL")
flag.StringVar(&o.DefaultNameServer, "default-ns", o.DefaultNameServer, "Adress to the default name server")
flag.Var(&o.StorageEngine, "storage-engine", fmt.Sprintf("Select the storage engine between %v", storage.GetStorageEngines()))
flag.BoolVar(&o.NoAuth, "no-auth", false, "Disable user access control, use default account")
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations
}

View File

@ -66,6 +66,9 @@ type Options struct {
// StorageEngine points to the storage engine used.
StorageEngine storage.StorageEngine
// NoAuth controls if there is user access control or not.
NoAuth bool
}
// BuildURL appends the given url to the absolute ExternalURL.

View File

@ -51,9 +51,12 @@
<b-nav-item-dropdown v-if="user_isLogged" right>
<template slot="button-content">
<b-button size="sm" variant="dark">
<b-button v-if="user_getSession.email !== '_no_auth'" size="sm" variant="dark">
<b-icon icon="person" aria-hidden="true" /> {{ user_getSession.email }}
</b-button>
<b-button v-else size="sm" variant="secondary">
{{ $t('menu.quick-menu') }}
</b-button>
</template>
<b-dropdown-item to="/domains/">
{{ $t('menu.my-domains') }}
@ -69,8 +72,8 @@
<b-dropdown-item to="/me">
{{ $t('menu.my-account') }}
</b-dropdown-item>
<b-dropdown-divider />
<b-dropdown-item @click="logout()">
<b-dropdown-divider v-if="user_getSession.email !== '_no_auth'" />
<b-dropdown-item v-if="user_getSession.email !== '_no_auth'" @click="logout()">
{{ $t('menu.logout') }}
</b-dropdown-item>
</b-nav-item-dropdown>

View File

@ -140,6 +140,7 @@
"content": "The page you are look for was not found."
},
"account-delete": "An error occurs when trying to delete your account",
"account-no-auth": "You're using happyDNS without authentication. You cannot manage other account properties.",
"address": "Email address is required",
"address-valid": "A valid email address is required",
"domain-access": "An error occurs when trying to access domain's list.",
@ -173,6 +174,7 @@
"dns-resolver": "DNS resolver",
"my-account": "My account",
"logout": "Logout",
"quick-menu": "Quick Access",
"signup": "Sign up",
"signin": "Sign in"
},

View File

@ -63,85 +63,90 @@
</b-form>
</b-card>
</b-row>
<h2 id="password-change">
{{ $t('password.change') }}
</h2>
<b-row>
<b-card class="offset-md-2 col-8">
<b-form @submit.stop.prevent="sendChPassword">
<b-form-group
:label="$t('password.enter')"
label-for="currentPassword-input"
>
<b-form-input
id="currentPassword-input"
v-model="signupForm.current"
type="password"
required
placeholder="xXxXxXxXxX"
autocomplete="current-password"
/>
</b-form-group>
<b-form-group
:state="passwordState"
:label="$t('password.enter-new')"
label-for="password-input"
:invalid-feedback="$t('errors.password-weak')"
>
<b-form-input
id="password-input"
ref="signuppassword"
v-model="signupForm.password"
type="password"
<div v-if="loggedUser && loggedUser.email !== '_no_auth'">
<h2 id="password-change">
{{ $t('password.change') }}
</h2>
<b-row>
<b-card class="offset-md-2 col-8">
<b-form @submit.stop.prevent="sendChPassword">
<b-form-group
:label="$t('password.enter')"
label-for="currentPassword-input"
>
<b-form-input
id="currentPassword-input"
v-model="signupForm.current"
type="password"
required
placeholder="xXxXxXxXxX"
autocomplete="current-password"
/>
</b-form-group>
<b-form-group
:state="passwordState"
required
placeholder="xXxXxXxXxX"
autocomplete="new-password"
/>
</b-form-group>
<b-form-group
:state="passwordConfirmState"
:label="$t('password.confirm-new')"
label-for="passwordconfirm-input"
:invalid-feedback="$t('errors.password-match')"
>
<b-form-input
id="passwordconfirm-input"
ref="signuppasswordconfirm"
v-model="signupForm.passwordConfirm"
type="password"
:label="$t('password.enter-new')"
label-for="password-input"
:invalid-feedback="$t('errors.password-weak')"
>
<b-form-input
id="password-input"
ref="signuppassword"
v-model="signupForm.password"
type="password"
:state="passwordState"
required
placeholder="xXxXxXxXxX"
autocomplete="new-password"
/>
</b-form-group>
<b-form-group
:state="passwordConfirmState"
required
placeholder="xXxXxXxXxX"
/>
</b-form-group>
<div class="d-flex justify-content-around">
<b-button type="submit" variant="primary">
{{ $t('password.change') }}
</b-button>
</div>
</b-form>
</b-card>
</b-row>
<hr>
<h2 id="delete-account">
{{ $t('account.delete.delete') }}
</h2>
<b-row>
<b-card class="offset-md-2 col-8">
<p>
{{ $t('account.delete.confirm') }}
</p>
<b-button type="button" variant="danger" @click="askAccountDeletion">
{{ $t('account.delete.delete') }}
</b-button>
<p class="mt-2 text-muted" style="line-height: 1.1">
<small>
{{ $t('account.delete.consequence') }}
</small>
</p>
</b-card>
</b-row>
:label="$t('password.confirm-new')"
label-for="passwordconfirm-input"
:invalid-feedback="$t('errors.password-match')"
>
<b-form-input
id="passwordconfirm-input"
ref="signuppasswordconfirm"
v-model="signupForm.passwordConfirm"
type="password"
:state="passwordConfirmState"
required
placeholder="xXxXxXxXxX"
/>
</b-form-group>
<div class="d-flex justify-content-around">
<b-button type="submit" variant="primary">
{{ $t('password.change') }}
</b-button>
</div>
</b-form>
</b-card>
</b-row>
<hr>
<h2 id="delete-account">
{{ $t('account.delete.delete') }}
</h2>
<b-row>
<b-card class="offset-md-2 col-8">
<p>
{{ $t('account.delete.confirm') }}
</p>
<b-button type="button" variant="danger" @click="askAccountDeletion">
{{ $t('account.delete.delete') }}
</b-button>
<p class="mt-2 text-muted" style="line-height: 1.1">
<small>
{{ $t('account.delete.consequence') }}
</small>
</p>
</b-card>
</b-row>
</div>
<div v-else class="m-5 alert alert-secondary">
{{ $t('errors.account-no-auth') }}
</div>
<b-modal id="delete-account-modal" :title="$t('account.delete.delete')" ok-variant="danger" :ok-title="$t('account.delete.delete')" cancel-variant="primary" @ok="deleteMyAccount">
<p>
{{ $t('account.delete.confirm-twice') }}

18
main.go
View File

@ -48,6 +48,7 @@ import (
"git.happydns.org/happydns/admin"
"git.happydns.org/happydns/api"
"git.happydns.org/happydns/config"
"git.happydns.org/happydns/model"
"git.happydns.org/happydns/storage"
_ "git.happydns.org/happydns/sources/alwaysdata"
@ -126,6 +127,23 @@ func main() {
}
}
if opts.NoAuth {
// Check if the default account exists.
if !storage.MainStore.UserExists(api.NO_AUTH_ACCOUNT) {
if user, err := happydns.NewUser(api.NO_AUTH_ACCOUNT, ""); err != nil {
log.Fatal("Unable to create default account:", err)
} else {
user.Settings = *happydns.DefaultUserSettings()
if err := storage.MainStore.CreateUser(user); err != nil {
log.Fatal("Unable to create default account in database:", err)
} else {
log.Println("Default account for NoAuth created.")
}
}
}
log.Println("WARNING: NoAuth option has to be use for testing or personnal purpose behind another restriction/authentication method.")
}
log.Println("Do database migrations...")
if err = storage.MainStore.DoMigration(); err != nil {
log.Fatal("Cannot migrate database: ", err)

View File

@ -80,7 +80,9 @@ func NewUser(email string, password string) (u *User, err error) {
RegistrationTime: &t,
}
err = u.DefinePassword(password)
if len(password) != 0 {
err = u.DefinePassword(password)
}
return
}
@ -118,6 +120,10 @@ func (u *User) DefinePassword(password string) (err error) {
// CheckAuth compares the given password to the hashed one in the User struct.
func (u *User) CheckAuth(password string) bool {
if len(password) < 8 {
return false
}
return bcrypt.CompareHashAndPassword(u.Password, []byte(password)) == nil
}