Implement user password checking

This commit is contained in:
Tom Wiesing 2022-11-25 15:06:01 +01:00
parent 8e2d2cce3e
commit 996ecb9f80
No known key found for this signature in database
25 changed files with 10762 additions and 224 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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;
}

View 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
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
// This file contains a list of common WissKI Passwords
W1ssk1.

View 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
}

View 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();
}

View file

@ -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) {