Implement user password checking
This commit is contained in:
parent
8e2d2cce3e
commit
996ecb9f80
25 changed files with 10762 additions and 224 deletions
|
|
@ -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