Implement user password checking
This commit is contained in:
parent
8e2d2cce3e
commit
996ecb9f80
25 changed files with 10762 additions and 224 deletions
|
|
@ -35,16 +35,7 @@ func ParamsFromEnv() (params Params, err error) {
|
|||
params.ConfigPath = filepath.Join(params.ConfigPath, bootstrap.ConfigFile)
|
||||
|
||||
// generate a new context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
params.Context = ctx
|
||||
|
||||
// cancel the context on an interrupt
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
<-c
|
||||
cancel()
|
||||
}()
|
||||
params.Context, _ = signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
|
||||
// and return the params!
|
||||
return params, nil
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package info
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
|
@ -12,6 +11,7 @@ import (
|
|||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
//go:embed "html/components.html"
|
||||
|
|
@ -51,12 +51,8 @@ type ingredientsContext struct {
|
|||
func (info *Info) ingredients(r *http.Request) (cp ingredientsContext, err error) {
|
||||
cp.Time = time.Now().UTC()
|
||||
|
||||
// find the slug as the last component of path!
|
||||
slug := strings.TrimSuffix(r.URL.Path, "/")
|
||||
slug = slug[strings.LastIndex(slug, "/")+1:]
|
||||
|
||||
// find the instance itself!
|
||||
instance, err := info.Instances.WissKI(slug)
|
||||
instance, err := info.Instances.WissKI(mux.Vars(r)["slug"])
|
||||
if err == instances.ErrWissKINotFound {
|
||||
return cp, httpx.ErrNotFound
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,28 +176,81 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-xl-2-5">
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="wisski">Users</h2>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
Users
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Active
|
||||
</th>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th>
|
||||
Roles
|
||||
</th>
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th>
|
||||
Last Login
|
||||
</th>
|
||||
<th>
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ $slug := .Instance.Slug }}
|
||||
{{ range $index, $user := .Info.Users }}
|
||||
<tr>
|
||||
<tr {{ if not $user.Status }}style="color:gray"{{ end }}>
|
||||
<td>
|
||||
<code>{{ $user }}</code>
|
||||
<code>{{ $user.UID }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<small>
|
||||
<button class="remote-link pure-button pure-button-action" role="link" data-action="login" data-params="{{ $slug }} {{ $user }} ">Login in new window</button>
|
||||
</small>
|
||||
<code>{{ $user.Status }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ $user.Name }}</code>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ if $user.Mail }}
|
||||
<a href="mailto:{{ $user.Mail }}">{{ $user.Mail }}</a>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
{{ range $role, $unuused := $user.Roles }}
|
||||
<code>
|
||||
{{ $role }}
|
||||
</code>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ $user.Created.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ $user.Login.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<form action="/dis/api/login" method="POST" target="_blank">
|
||||
<input type="hidden" name="slug" value="{{ $slug }}">
|
||||
<input type="hidden" name="user" value="{{ $user.Name }}">
|
||||
<input type="submit" class="pure-button pure-button-action" value="Login in new window">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
|
|
@ -27,50 +28,71 @@ type Info struct {
|
|||
|
||||
func (*Info) Routes() []string { return []string{"/dis/"} }
|
||||
|
||||
func (info *Info) Handler(route string, context context.Context, io stream.IOStream) (http.Handler, error) {
|
||||
mux := http.NewServeMux()
|
||||
func (info *Info) Handler(route string, context context.Context, io stream.IOStream) (handler http.Handler, err error) {
|
||||
router := mux.NewRouter()
|
||||
{
|
||||
socket := &httpx.WebSocket{
|
||||
Context: context,
|
||||
Fallback: router,
|
||||
Handler: info.serveSocket,
|
||||
}
|
||||
handler = httpx.BasicAuth(socket, "WissKI Distillery Admin", func(user, pass string) bool {
|
||||
return user == info.Config.DisAdminUser && pass == info.Config.DisAdminPassword
|
||||
})
|
||||
}
|
||||
|
||||
// handle everything
|
||||
mux.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == route {
|
||||
http.Redirect(w, r, route+"/index", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
router.Path(route).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, route+"/index", http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
// add a handler for the index page
|
||||
mux.Handle(route+"index", httpx.HTMLHandler[indexContext]{
|
||||
router.Path(route + "index").Handler(httpx.HTMLHandler[indexContext]{
|
||||
Handler: info.index,
|
||||
Template: indexTemplate,
|
||||
})
|
||||
|
||||
// add a handler for the component page
|
||||
mux.Handle(route+"components", httpx.HTMLHandler[componentContext]{
|
||||
router.Path(route + "components").Handler(httpx.HTMLHandler[componentContext]{
|
||||
Handler: info.components,
|
||||
Template: componentsTemplate,
|
||||
})
|
||||
|
||||
// add a handler for the component page
|
||||
mux.Handle(route+"ingredients/", httpx.HTMLHandler[ingredientsContext]{
|
||||
router.Path(route + "ingredients/{slug}").Handler(httpx.HTMLHandler[ingredientsContext]{
|
||||
Handler: info.ingredients,
|
||||
Template: ingredientsTemplate,
|
||||
})
|
||||
|
||||
// add a handler for the instance page
|
||||
mux.Handle(route+"instance/", httpx.HTMLHandler[instanceContext]{
|
||||
router.Path(route + "instance/{slug}").Handler(httpx.HTMLHandler[instanceContext]{
|
||||
Handler: info.instance,
|
||||
Template: instanceTemplate,
|
||||
})
|
||||
|
||||
handler := &httpx.WebSocket{
|
||||
Context: context,
|
||||
Fallback: mux,
|
||||
Handler: info.serveSocket,
|
||||
}
|
||||
router.Path(route + "api/login").Handler(httpx.ClientSideRedirect(func(r *http.Request) (string, error) {
|
||||
// enforce POST
|
||||
if r.Method != http.MethodPost {
|
||||
return "", httpx.ErrMethodNotAllowed
|
||||
}
|
||||
|
||||
// ensure that everyone is logged in!
|
||||
return httpx.BasicAuth(handler, "WissKI Distillery Admin", func(user, pass string) bool {
|
||||
return user == info.Config.DisAdminUser && pass == info.Config.DisAdminPassword
|
||||
}), nil
|
||||
// parse the form
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// get the instance
|
||||
instance, err := info.Instances.WissKI(r.PostFormValue("slug"))
|
||||
if err != nil {
|
||||
return "", httpx.ErrNotFound
|
||||
}
|
||||
|
||||
target, err := instance.Users().Login(nil, r.PostFormValue("user"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return target.String(), err
|
||||
}))
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package info
|
|||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
||||
|
|
@ -11,6 +10,7 @@ import (
|
|||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/status"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
//go:embed "html/instance.html"
|
||||
|
|
@ -28,12 +28,8 @@ type instanceContext struct {
|
|||
}
|
||||
|
||||
func (info *Info) instance(r *http.Request) (is instanceContext, err error) {
|
||||
// find the slug as the last component of path!
|
||||
slug := strings.TrimSuffix(r.URL.Path, "/")
|
||||
slug = slug[strings.LastIndex(slug, "/")+1:]
|
||||
|
||||
// find the instance itself!
|
||||
instance, err := info.Instances.WissKI(slug)
|
||||
instance, err := info.Instances.WissKI(mux.Vars(r)["slug"])
|
||||
if err == instances.ErrWissKINotFound {
|
||||
return is, httpx.ErrNotFound
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,13 +47,6 @@ var socketInstanceActions = map[string]InstanceAction{
|
|||
return instance.Drush().Cron(str)
|
||||
},
|
||||
},
|
||||
"login": {
|
||||
NumParams: 1,
|
||||
HandleResult: func(_ *Info, instance *wisski.WissKI, params ...string) (any, error) {
|
||||
link, err := instance.Drush().Login(stream.FromNil(), params[0])
|
||||
return link, err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (info *Info) serveSocket(conn httpx.WebSocketConnection) {
|
||||
|
|
|
|||
|
|
@ -2,52 +2,29 @@ package phpx
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PHPBoolean represents a boolean php value.
|
||||
// Boolean represents a boolean php value.
|
||||
//
|
||||
// The value can be marshaled to and from php and will behave as a PHP would behave.
|
||||
//
|
||||
// The value will always be marshaled as "true" or "false".
|
||||
// Unmarshaling uses [Boolean].
|
||||
type PHPBoolean bool
|
||||
// Unmarshaling uses [AsBoolean].
|
||||
type Boolean bool
|
||||
|
||||
func (bi PHPBoolean) MarshalJSON() ([]byte, error) {
|
||||
if bi {
|
||||
return []byte("true"), nil
|
||||
}
|
||||
return []byte("false"), nil
|
||||
}
|
||||
func (bi *PHPBoolean) UnmarshalJSON(data []byte) (err error) {
|
||||
// unmarshal into a generic value
|
||||
var value any
|
||||
err = json.Unmarshal(data, &value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// cast into a boolean
|
||||
cast, ok := Boolean(value)
|
||||
if !ok {
|
||||
value = false
|
||||
}
|
||||
*bi = PHPBoolean(cast)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Boolean tries to cast the given value to a boolean.
|
||||
// AsBoolean tries to cast the given value to a boolean.
|
||||
//
|
||||
// It is able to handle any value that would be [json.Unmarshaled] from a corresponding PHP value.
|
||||
// Value treates all values as the boolean true, except for the ones listed at [doc].
|
||||
//
|
||||
// [doc]: https://www.php.net/manual/en/language.types.boolean.php#language.types.boolean.casting
|
||||
func Boolean(value any) (b bool, ok bool) {
|
||||
func AsBoolean(value any) (b Boolean, ok bool) {
|
||||
switch d := value.(type) {
|
||||
case bool:
|
||||
return d, true
|
||||
return Boolean(d), true
|
||||
case float64:
|
||||
return d != 0, true
|
||||
case string:
|
||||
|
|
@ -62,13 +39,40 @@ func Boolean(value any) (b bool, ok bool) {
|
|||
return true, false
|
||||
}
|
||||
|
||||
// String tries to cast the given value to a string.
|
||||
func (b Boolean) MarshalJSON() ([]byte, error) {
|
||||
if b {
|
||||
return []byte("true"), nil
|
||||
}
|
||||
return []byte("false"), nil
|
||||
}
|
||||
|
||||
var errNotABoolean = errors.New("Boolean.UnmarshalJSON: Not an integer")
|
||||
|
||||
func (b *Boolean) UnmarshalJSON(data []byte) (err error) {
|
||||
return UnmarshalIntermediate(b, func(a any) (Boolean, error) {
|
||||
b, ok := AsBoolean(a)
|
||||
if !ok {
|
||||
return Boolean(false), errNotABoolean
|
||||
}
|
||||
return b, nil
|
||||
}, data)
|
||||
}
|
||||
|
||||
// String represents a string php value.
|
||||
//
|
||||
// The value can be marshaled to and from php and will behave as a PHP would behave.
|
||||
//
|
||||
// The value will always be marshaled as a literal string.
|
||||
// Unmarshaling uses [AsString].
|
||||
type String string
|
||||
|
||||
// AsString tries to cast the given value to a string.
|
||||
//
|
||||
// It is able to handle any value that would be [json.Unmarshaled] from a corresponding PHP value.
|
||||
// Value casting is described at [doc].
|
||||
//
|
||||
// [doc]: https://www.php.net/manual/en/language.types.string.php#language.types.string.casting
|
||||
func String(value any) (s string, ok bool) {
|
||||
func AsString(value any) (s String, ok bool) {
|
||||
switch d := value.(type) {
|
||||
case bool:
|
||||
if d {
|
||||
|
|
@ -77,13 +81,12 @@ func String(value any) (s string, ok bool) {
|
|||
return "", true
|
||||
case float64:
|
||||
if d == float64(int64(d)) {
|
||||
return strconv.FormatInt(int64(d), 10), true
|
||||
return String(strconv.FormatInt(int64(d), 10)), true
|
||||
}
|
||||
// TODO: not sure this is entirely correct
|
||||
// and we should handle ints here!
|
||||
return strconv.FormatFloat(d, 'E', 1, 64), true
|
||||
return String(strconv.FormatFloat(d, 'E', 1, 64)), true
|
||||
case string:
|
||||
return d, true
|
||||
return String(d), true
|
||||
case []any, map[string]any:
|
||||
return "Array", true
|
||||
case nil:
|
||||
|
|
@ -93,14 +96,38 @@ func String(value any) (s string, ok bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
// Integer tries to cast the given value to an integer.
|
||||
func (s String) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(string(s))
|
||||
}
|
||||
|
||||
var errNotAString = errors.New("String.UnmarshalJSON: Not a string")
|
||||
|
||||
func (s *String) UnmarshalJSON(data []byte) (err error) {
|
||||
return UnmarshalIntermediate(s, func(a any) (String, error) {
|
||||
s, ok := AsString(a)
|
||||
if !ok {
|
||||
return s, errNotAString
|
||||
}
|
||||
return s, nil
|
||||
}, data)
|
||||
}
|
||||
|
||||
// Integer represents a boolean integer value.
|
||||
//
|
||||
// The value can be marshaled to and from php and will behave as a PHP would behave.
|
||||
//
|
||||
// The value will always be marshaled as an integer directly
|
||||
// Unmarshaling uses [AsInteger].
|
||||
type Integer int64
|
||||
|
||||
// AsInteger tries to cast the given value to an integer.
|
||||
//
|
||||
// It is able to handle any value that would be [json.Unmarshaled] from a corresponding PHP value.
|
||||
// Value casting is described at [doc].
|
||||
//
|
||||
// [doc]: https://www.php.net/manual/en/language.types.integer.php#language.types.integer.casting
|
||||
func Integer(value any) (i int64, ok bool) {
|
||||
str, ok := String(value)
|
||||
func AsInteger(value any) (i Integer, ok bool) {
|
||||
str, ok := AsString(value)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
|
@ -108,31 +135,62 @@ func Integer(value any) (i int64, ok bool) {
|
|||
// try to parse the "leading" string, by successively cutting off parts of the tail
|
||||
// once we have a valid number, return it.
|
||||
for l := 0; l < len(str); l++ {
|
||||
i64, err := strconv.ParseInt(str[:len(str)-l], 10, 64)
|
||||
i64, err := strconv.ParseInt(string(str)[:len(str)-l], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return i64, true
|
||||
return Integer(i64), true
|
||||
}
|
||||
return 0, true
|
||||
}
|
||||
|
||||
// TimeInt represents a time value in PHP, represented as an integer
|
||||
type TimeInt time.Time
|
||||
func (i Integer) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(int64(i))
|
||||
}
|
||||
|
||||
func (ts TimeInt) Time() time.Time {
|
||||
var errNotAnInteger = errors.New("Integer.UnmarshalJSON: Not an integer")
|
||||
|
||||
func (i *Integer) UnmarshalJSON(data []byte) (err error) {
|
||||
return UnmarshalIntermediate(i, func(a any) (Integer, error) {
|
||||
i, ok := AsInteger(a)
|
||||
if !ok {
|
||||
return i, errNotAnInteger
|
||||
}
|
||||
return i, nil
|
||||
}, data)
|
||||
}
|
||||
|
||||
// Timestamp represents a time value in PHP, represented as an integer
|
||||
type Timestamp time.Time
|
||||
|
||||
func (ts Timestamp) Time() time.Time {
|
||||
return time.Time(ts)
|
||||
}
|
||||
func (ts TimeInt) MarshalJSON() ([]byte, error) {
|
||||
func (ts Timestamp) MarshalJSON() ([]byte, error) {
|
||||
return []byte(strconv.FormatInt(ts.Time().Unix(), 10)), nil
|
||||
}
|
||||
|
||||
func (ts *TimeInt) UnmarshalJSON(data []byte) (err error) {
|
||||
var value any
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
func (ts *Timestamp) UnmarshalJSON(data []byte) (err error) {
|
||||
return UnmarshalIntermediate(ts, func(value Integer) (Timestamp, error) {
|
||||
return Timestamp(time.Unix(int64(value), 0)), nil
|
||||
}, data)
|
||||
}
|
||||
|
||||
// UnmarshalIntermediate unmarshals src into dest using an intermediate value of type I.
|
||||
//
|
||||
// It first unmarshals src into a new value of type I.
|
||||
// It then calls parser to parse I into T.
|
||||
func UnmarshalIntermediate[I, T any](dest *T, parser func(I) (T, error), src []byte) (err error) {
|
||||
var temp I
|
||||
err = json.Unmarshal(src, &temp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unix, _ := Integer(value)
|
||||
*ts = TimeInt(time.Unix(unix, 0))
|
||||
|
||||
*dest, err = parser(temp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ type WissKI struct {
|
|||
NoPrefixes bool // TODO: Move this into the database
|
||||
Prefixes []string // list of prefixes
|
||||
Pathbuilders map[string]string // all the pathbuilders
|
||||
Users []string // all the known users
|
||||
Users []User // all the known users
|
||||
}
|
||||
|
||||
// Statistics holds statistics generated by the WissKI module
|
||||
|
|
@ -70,9 +70,9 @@ type BundleStatistics struct {
|
|||
|
||||
Count int `json:"entities"`
|
||||
|
||||
LastEdit phpx.TimeInt `json:"lastEdit"`
|
||||
LastEdit phpx.Timestamp `json:"lastEdit"`
|
||||
|
||||
MainBundle phpx.PHPBoolean `json:"mainBundle"`
|
||||
MainBundle phpx.Boolean `json:"mainBundle"`
|
||||
} `json:"bundleStatistics"`
|
||||
TotalBundles int `json:"totalBundles"`
|
||||
TotalMainBundles int `json:"totalMainBundles"`
|
||||
|
|
|
|||
65
internal/status/wisski_user.go
Normal file
65
internal/status/wisski_user.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/phpx"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// User represents a WissKI User
|
||||
type User struct {
|
||||
UID phpx.Integer `json:"uid,omitempty"`
|
||||
Name phpx.String `json:"name,omitempty"`
|
||||
Mail phpx.String `json:"mail,omitempty"`
|
||||
Status phpx.Boolean `json:"status,omitempty"`
|
||||
Created phpx.Timestamp `json:"created,omitempty"`
|
||||
Changed phpx.Timestamp `json:"changed,omitempty"`
|
||||
Access phpx.Timestamp `json:"access,omitempty"`
|
||||
Login phpx.Timestamp `json:"login,omitempty"`
|
||||
Roles UserRoles `json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
// UserRole represents the role of a user
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
Administrator UserRole = "administrator"
|
||||
ContentEditor UserRole = "content_editor"
|
||||
)
|
||||
|
||||
// UserRoles represents a set of user roles for a given user
|
||||
type UserRoles map[UserRole]struct{}
|
||||
|
||||
// Has checks if the UserRole has the given role
|
||||
func (ur UserRoles) Has(role UserRole) (ok bool) {
|
||||
_, ok = ur[role]
|
||||
return
|
||||
}
|
||||
|
||||
func (ur UserRoles) MarshalJSON() ([]byte, error) {
|
||||
roles := make([]string, len(ur))
|
||||
i := 0
|
||||
for r := range ur {
|
||||
roles[i] = string(r)
|
||||
i++
|
||||
}
|
||||
slices.Sort(roles) // for consistent marshaling
|
||||
|
||||
return json.Marshal(strings.Join(roles, ", "))
|
||||
}
|
||||
|
||||
func (u *UserRoles) UnmarshalJSON(data []byte) error {
|
||||
return phpx.UnmarshalIntermediate(u, func(s phpx.String) (UserRoles, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
roles := strings.Split(string(s), ", ")
|
||||
uroles := make(UserRoles, len(roles))
|
||||
for _, r := range roles {
|
||||
uroles[UserRole(r)] = struct{}{}
|
||||
}
|
||||
return uroles, nil
|
||||
}, data)
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package drush
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/tkw1536/goprogram/exit"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
var errLoginFailed = exit.Error{
|
||||
Message: "Failed to login",
|
||||
ExitCode: exit.ExitGeneric,
|
||||
}
|
||||
|
||||
// Login generates a one-time login url for the given user
|
||||
func (drush *Drush) Login(io stream.IOStream, user string) (string, error) {
|
||||
var builder strings.Builder
|
||||
|
||||
url := drush.Liquid.URL().String()
|
||||
command := shellescape.QuoteCommand([]string{"drush", "user:login", "--name=" + user, "--no-browser", "--uri=" + url})
|
||||
|
||||
code, err := drush.Barrel.Shell(io.Streams(&builder, nil, nil, 0), "-c", command)
|
||||
if code != 0 || err != nil {
|
||||
return "", errLoginFailed
|
||||
}
|
||||
return strings.TrimSpace(builder.String()), nil
|
||||
}
|
||||
|
||||
var errSetPasswordFailed = exit.Error{
|
||||
Message: "Failed to set password",
|
||||
ExitCode: exit.ExitGeneric,
|
||||
}
|
||||
|
||||
func (drush *Drush) ResetPassword(io stream.IOStream, user, password string) error {
|
||||
code, err := drush.Barrel.Shell(io, "-c", "drush", "user:password", user, password)
|
||||
if code != 0 || err != nil {
|
||||
return errSetPasswordFailed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package extras
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/phpx"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/status"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php"
|
||||
)
|
||||
|
||||
type Users struct {
|
||||
ingredient.Base
|
||||
|
||||
PHP *php.PHP
|
||||
}
|
||||
|
||||
//go:embed users.php
|
||||
var usersPHP string
|
||||
|
||||
// All returns all known usernames
|
||||
func (u *Users) All(server *phpx.Server) (users []string, err error) {
|
||||
err = u.PHP.ExecScript(server, &users, usersPHP, "list_users")
|
||||
return
|
||||
}
|
||||
|
||||
func (u *Users) Fetch(flags ingredient.FetcherFlags, info *status.WissKI) (err error) {
|
||||
if flags.Quick {
|
||||
return
|
||||
}
|
||||
|
||||
info.Users, _ = u.All(flags.Server)
|
||||
return
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/** lists all the users */
|
||||
function list_users() {
|
||||
|
||||
$usernames = [];
|
||||
$users = User::loadMultiple(NULL);
|
||||
foreach($users as $user){
|
||||
$name = $user->get('name')->getString();
|
||||
if(empty($name)) continue;
|
||||
$usernames[] = $name;
|
||||
}
|
||||
return $usernames;
|
||||
}
|
||||
144
internal/wisski/ingredient/php/users/password.go
Normal file
144
internal/wisski/ingredient/php/users/password.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/phpx"
|
||||
)
|
||||
|
||||
var errGetValidator = errors.New("GetPasswordValidator: Unknown Error")
|
||||
|
||||
func (u *Users) GetPasswordValidator(username string) (pv PasswordValidator, err error) {
|
||||
server := u.PHP.NewServer()
|
||||
|
||||
var hash string
|
||||
err = u.PHP.ExecScript(server, &hash, usersPHP, "get_password_hash", username)
|
||||
if err != nil {
|
||||
server.Close()
|
||||
return pv, err
|
||||
}
|
||||
if len(hash) == 0 {
|
||||
server.Close()
|
||||
return pv, errGetValidator
|
||||
}
|
||||
|
||||
pv.server = server
|
||||
pv.username = username
|
||||
pv.hash = hash
|
||||
return pv, nil
|
||||
}
|
||||
|
||||
type PasswordValidator struct {
|
||||
server *phpx.Server
|
||||
|
||||
username string
|
||||
hash string
|
||||
}
|
||||
|
||||
func (pv PasswordValidator) Close() error {
|
||||
return pv.server.Close()
|
||||
}
|
||||
|
||||
func (pv PasswordValidator) Check(password string) bool {
|
||||
var result phpx.Boolean
|
||||
err := pv.server.MarshalCall(&result, "check_password_hash", password, string(pv.hash))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return bool(result)
|
||||
}
|
||||
|
||||
var errPasswordUsername = errors.New("username === password")
|
||||
|
||||
type CommonPasswordError struct {
|
||||
Password CommonPassword
|
||||
}
|
||||
|
||||
func (cpe CommonPasswordError) Error() string {
|
||||
return fmt.Sprintf("%q from %q", cpe.Password.Password, cpe.Password.Source)
|
||||
}
|
||||
|
||||
func (pv PasswordValidator) CheckDictionary(context context.Context, writer io.Writer) error {
|
||||
var counter int
|
||||
|
||||
if pv.Check(pv.username) {
|
||||
if writer != nil {
|
||||
counter++
|
||||
fmt.Fprintln(writer, counter)
|
||||
}
|
||||
return errPasswordUsername
|
||||
}
|
||||
for candidate := range CommonPasswords() {
|
||||
if context.Err() != nil {
|
||||
continue
|
||||
}
|
||||
result := pv.Check(candidate.Password)
|
||||
if writer != nil {
|
||||
counter++
|
||||
fmt.Fprintln(writer, counter)
|
||||
}
|
||||
|
||||
if result {
|
||||
return &CommonPasswordError{Password: candidate}
|
||||
}
|
||||
}
|
||||
|
||||
return context.Err()
|
||||
}
|
||||
|
||||
//go:embed passwords
|
||||
var passwordsEmbed embed.FS
|
||||
|
||||
type CommonPassword struct {
|
||||
Password string
|
||||
Source string
|
||||
}
|
||||
|
||||
// CommonPasswords returns a channel of most common passwords
|
||||
func CommonPasswords() <-chan CommonPassword {
|
||||
pChan := make(chan CommonPassword, 10)
|
||||
go func() {
|
||||
defer close(pChan)
|
||||
|
||||
fs.WalkDir(passwordsEmbed, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the full path
|
||||
if d.IsDir() || !strings.HasSuffix(path, ".txt") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// open it
|
||||
file, err := passwordsEmbed.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// scan it line by line
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
}
|
||||
pChan <- CommonPassword{
|
||||
Password: line,
|
||||
Source: path,
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
})
|
||||
}()
|
||||
return pChan
|
||||
}
|
||||
10025
internal/wisski/ingredient/php/users/passwords/top10_000.txt
Normal file
10025
internal/wisski/ingredient/php/users/passwords/top10_000.txt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,2 @@
|
|||
// This file contains a list of common WissKI Passwords
|
||||
W1ssk1.
|
||||
79
internal/wisski/ingredient/php/users/users.go
Normal file
79
internal/wisski/ingredient/php/users/users.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/phpx"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/status"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php"
|
||||
)
|
||||
|
||||
type Users struct {
|
||||
ingredient.Base
|
||||
|
||||
PHP *php.PHP
|
||||
}
|
||||
|
||||
//go:embed users.php
|
||||
var usersPHP string
|
||||
|
||||
// All returns all known usernames
|
||||
func (u *Users) All(server *phpx.Server) (users []status.User, err error) {
|
||||
err = u.PHP.ExecScript(server, &users, usersPHP, "list_users")
|
||||
return
|
||||
}
|
||||
|
||||
var errLoginUnknownError = errors.New("Login: Unknown Error")
|
||||
|
||||
// Login generates a login link for the user with the given username
|
||||
func (u *Users) Login(server *phpx.Server, username string) (dest *url.URL, err error) {
|
||||
|
||||
// generate a (relative) link
|
||||
var path string
|
||||
err = u.PHP.ExecScript(server, &path, usersPHP, "get_login_link", username)
|
||||
|
||||
// if something went wrong, return
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if path == "" {
|
||||
return nil, errLoginUnknownError
|
||||
}
|
||||
|
||||
// parse it as a url
|
||||
dest, err = url.Parse(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// and resolve the (possibly relative) reference
|
||||
dest = u.URL().ResolveReference(dest)
|
||||
return
|
||||
}
|
||||
|
||||
var errSetPassword = errors.New("SetPassword: Unknown Error")
|
||||
|
||||
// SetPassword sets the password for a given user
|
||||
func (u *Users) SetPassword(server *phpx.Server, username, password string) error {
|
||||
var ok bool
|
||||
err := u.PHP.ExecScript(server, &ok, usersPHP, "set_user_password", username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return errSetPassword
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Users) Fetch(flags ingredient.FetcherFlags, info *status.WissKI) (err error) {
|
||||
if flags.Quick {
|
||||
return
|
||||
}
|
||||
|
||||
info.Users, _ = u.All(flags.Server)
|
||||
return
|
||||
}
|
||||
56
internal/wisski/ingredient/php/users/users.php
Normal file
56
internal/wisski/ingredient/php/users/users.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/** lists all the users */
|
||||
function list_users(): mixed {
|
||||
$users = [];
|
||||
foreach (User::loadMultiple(NULL) as $user) {
|
||||
$fields = array_map(function ($field) {
|
||||
return $field->getString();
|
||||
}, $user->getFields());
|
||||
if (empty($fields['name'])) continue;
|
||||
$users[] = $fields;
|
||||
}
|
||||
return $users;
|
||||
}
|
||||
|
||||
function set_user_password($name, $password): bool {
|
||||
$user = user_load_by_name($name);
|
||||
if (!$user) return false;
|
||||
$user->setPassword($password);
|
||||
$user->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
function get_password_hash($name): string {
|
||||
$user = user_load_by_name($name);
|
||||
if (!$user) return "";
|
||||
return $user->get('pass')->getString();
|
||||
}
|
||||
|
||||
|
||||
function check_password_hash($password, $hash): bool {
|
||||
return \Drupal::service('password')->check($password, $hash);
|
||||
}
|
||||
|
||||
function get_login_link($name): string {
|
||||
$account = user_load_by_name($name);
|
||||
if (!$account) return "";
|
||||
|
||||
$timestamp = \Drupal::time()->getRequestTime();
|
||||
return Url::fromRoute(
|
||||
'user.reset.login',
|
||||
[
|
||||
'uid' => $account->id(),
|
||||
'timestamp' => $timestamp,
|
||||
'hash' => user_pass_rehash($account, $timestamp),
|
||||
],
|
||||
[
|
||||
'absolute' => false,
|
||||
'query' => ['destination' => '/'],
|
||||
'language' => \Drupal::languageManager()->getLanguage($account->getPreferredLangcode()),
|
||||
]
|
||||
)->toString();
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/mstore"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/extras"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/users"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/reserve"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/liquid"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
|
||||
|
|
@ -61,6 +62,10 @@ func (wisski *WissKI) Drush() *drush.Drush {
|
|||
return export[*drush.Drush](wisski)
|
||||
}
|
||||
|
||||
func (wisski *WissKI) Users() *users.Users {
|
||||
return export[*users.Users](wisski)
|
||||
}
|
||||
|
||||
func (wisski *WissKI) Prefixes() *extras.Prefixes {
|
||||
return export[*extras.Prefixes](wisski)
|
||||
}
|
||||
|
|
@ -99,8 +104,8 @@ func (wisski *WissKI) allIngredients() []initFunc {
|
|||
auto[*extras.Prefixes],
|
||||
auto[*extras.Settings],
|
||||
auto[*extras.Pathbuilder],
|
||||
auto[*extras.Users],
|
||||
auto[*extras.Stats],
|
||||
auto[*users.Users],
|
||||
|
||||
// info
|
||||
manual(func(info *info.Info) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue