Expose user login functionality
This commit is contained in:
parent
97f5ac7e1a
commit
8a5b066839
8 changed files with 246 additions and 8 deletions
12
NEWS.md
12
NEWS.md
|
|
@ -2,6 +2,18 @@
|
|||
|
||||
This file contains signficant news items for the distillery.
|
||||
|
||||
# User And Instance Management (2023-01-07)
|
||||
- the concept of distillery user accounts has been added
|
||||
- their accounts have a password as well as TOTP
|
||||
- users can manage their own account details
|
||||
- administrators can reset user passwords, and disable TOTP
|
||||
- distillery accounts can be linked to multiple drupal accounts
|
||||
- users can sign into the account without entering further passwords
|
||||
- users must have two-factor-authentication enabled to use this functionality
|
||||
- administrators have access to the distillery admin panel
|
||||
- the functionality to manage distillery accounts has been added
|
||||
- the functionality to link distillery and drupal accounts has been added
|
||||
|
||||
# Automatic Password Checking (2022-11-25)
|
||||
- Implemented automatic password checking
|
||||
|
||||
|
|
|
|||
109
internal/dis/component/auth/next/next.go
Normal file
109
internal/dis/component/auth/next/next.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package next
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/users"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
)
|
||||
|
||||
type Next struct {
|
||||
component.Base
|
||||
Dependencies struct {
|
||||
Auth *auth.Auth
|
||||
Policy *policy.Policy
|
||||
Instances *instances.Instances
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ component.Routeable = (*Next)(nil)
|
||||
)
|
||||
|
||||
func (next *Next) Routes() component.Routes {
|
||||
return component.Routes{
|
||||
Paths: []string{"/next/"},
|
||||
Decorator: next.Dependencies.Auth.Require(auth.User),
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns a url that will forward authorized users to the given slug and path
|
||||
func (next *Next) Next(context context.Context, slug, path string) (string, error) {
|
||||
wisski, err := next.Dependencies.Instances.WissKI(context, slug)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
target := wisski.URL()
|
||||
target.Path = path
|
||||
return "/next/?next=" + url.PathEscape(target.String()), nil
|
||||
|
||||
}
|
||||
|
||||
func (next *Next) getInstance(r *http.Request) (wisski *wisski.WissKI, path string, err error) {
|
||||
// extract the instance
|
||||
url, err := url.Parse(r.URL.Query().Get("next"))
|
||||
if err != nil {
|
||||
return nil, "", httpx.ErrBadRequest
|
||||
}
|
||||
|
||||
// find the slug
|
||||
slug, ok := next.Config.SlugFromHost(url.Host)
|
||||
if slug == "" || !ok {
|
||||
return nil, "", httpx.ErrBadRequest
|
||||
}
|
||||
|
||||
// fetch the instance from the database
|
||||
wisski, err = next.Dependencies.Instances.WissKI(r.Context(), slug)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// return the wisski and the relative path
|
||||
return wisski, url.Path, nil
|
||||
}
|
||||
|
||||
func (next *Next) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
|
||||
return httpx.RedirectHandler(func(r *http.Request) (string, int, error) {
|
||||
// get the instance and the path
|
||||
instance, path, err := next.getInstance(r)
|
||||
if err != nil {
|
||||
return "", 0, httpx.ErrForbidden
|
||||
}
|
||||
|
||||
// get the user
|
||||
user, err := next.Dependencies.Auth.UserOf(r)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
// check if they have a grant
|
||||
grant, err := next.Dependencies.Policy.Has(r.Context(), user.User.User, instance.Slug)
|
||||
if err == policy.ErrNoAccess {
|
||||
return "", 0, httpx.ErrForbidden
|
||||
}
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
// perform the login
|
||||
dest, err := instance.Users().LoginWithOpt(r.Context(), nil, grant.DrupalUsername, users.LoginOptions{
|
||||
Destination: path,
|
||||
CreateIfMissing: true,
|
||||
GrantAdminRole: grant.DrupalAdminRole,
|
||||
})
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
// and redirect
|
||||
return dest.String(), http.StatusSeeOther, nil
|
||||
}), nil
|
||||
}
|
||||
|
|
@ -6,7 +6,10 @@ import (
|
|||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/next"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
|
@ -15,8 +18,11 @@ import (
|
|||
type UserPanel struct {
|
||||
component.Base
|
||||
Dependencies struct {
|
||||
Auth *auth.Auth
|
||||
Custom *custom.Custom
|
||||
Auth *auth.Auth
|
||||
Custom *custom.Custom
|
||||
Policy *policy.Policy
|
||||
Instances *instances.Instances
|
||||
Next *next.Next
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,8 +55,49 @@
|
|||
{{ end }}
|
||||
|
||||
<div class="pure-u-1">
|
||||
There will be a list of WissKIs you have access to here.
|
||||
<h2>Your WissKIs</h2>
|
||||
<p>
|
||||
This is a page of WissKIs you have access to.
|
||||
Click on the button containing the name to login.
|
||||
</p>
|
||||
</div>
|
||||
<div class="pure-u-1">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered pure-form">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
WissKI Slug
|
||||
</th>
|
||||
<th>
|
||||
Drupal Username
|
||||
</th>
|
||||
<th>
|
||||
Admin
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $id, $grant := .Grants }}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ $grant.URL }}" class="pure-button" target="_blank" rel="noopener noreferer">
|
||||
{{ $grant.Slug }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ $grant.DrupalUsername }}
|
||||
</td>
|
||||
<td>
|
||||
{{ $grant.DrupalAdminRole }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{ end }}
|
||||
|
|
@ -2,6 +2,7 @@ package panel
|
|||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
_ "embed"
|
||||
|
|
@ -9,6 +10,7 @@ import (
|
|||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
)
|
||||
|
||||
|
|
@ -22,6 +24,13 @@ var userTemplate = static.AssetsUser.MustParseShared(
|
|||
type routeUserContext struct {
|
||||
custom.BaseContext
|
||||
*auth.AuthUser
|
||||
|
||||
Grants []GrantWithURL
|
||||
}
|
||||
|
||||
type GrantWithURL struct {
|
||||
models.Grant
|
||||
URL template.URL
|
||||
}
|
||||
|
||||
func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
|
||||
|
|
@ -29,7 +38,30 @@ func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
|
|||
return &httpx.HTMLHandler[routeUserContext]{
|
||||
Handler: func(r *http.Request) (ruc routeUserContext, err error) {
|
||||
panel.Dependencies.Custom.Update(&ruc, r)
|
||||
|
||||
// find the user
|
||||
ruc.AuthUser, err = panel.Dependencies.Auth.UserOf(r)
|
||||
if err != nil || ruc.AuthUser == nil {
|
||||
return ruc, err
|
||||
}
|
||||
|
||||
// find the grants
|
||||
grants, err := panel.Dependencies.Policy.User(r.Context(), ruc.AuthUser.User.User)
|
||||
if err != nil {
|
||||
return ruc, err
|
||||
}
|
||||
|
||||
ruc.Grants = make([]GrantWithURL, len(grants))
|
||||
for i, grant := range grants {
|
||||
ruc.Grants[i].Grant = grant
|
||||
|
||||
url, err := panel.Dependencies.Next.Next(r.Context(), grant.Slug, "/")
|
||||
if err != nil {
|
||||
return ruc, err
|
||||
}
|
||||
ruc.Grants[i].URL = template.URL(url)
|
||||
}
|
||||
|
||||
return ruc, err
|
||||
},
|
||||
Template: userTemplate,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/next"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/panel"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control"
|
||||
|
|
@ -139,6 +140,7 @@ func (dis *Distillery) allComponents() []initFunc {
|
|||
auto[*auth.Auth],
|
||||
auto[*policy.Policy],
|
||||
auto[*panel.UserPanel],
|
||||
auto[*next.Next],
|
||||
|
||||
// instances
|
||||
auto[*instances.Instances],
|
||||
|
|
|
|||
|
|
@ -36,10 +36,25 @@ var errLoginUnknownError = errors.New("Login: Unknown Error")
|
|||
|
||||
// Login generates a login link for the user with the given username
|
||||
func (u *Users) Login(ctx context.Context, server *phpx.Server, username string) (dest *url.URL, err error) {
|
||||
return u.LoginWithOpt(ctx, server, username, LoginOptions{
|
||||
Destination: "/",
|
||||
CreateIfMissing: false,
|
||||
GrantAdminRole: false,
|
||||
})
|
||||
}
|
||||
|
||||
type LoginOptions struct {
|
||||
Destination string
|
||||
CreateIfMissing bool
|
||||
GrantAdminRole bool
|
||||
}
|
||||
|
||||
// LoginOrCreate generates a login link for the user with the given username and options
|
||||
func (u *Users) LoginWithOpt(ctx context.Context, server *phpx.Server, username string, opts LoginOptions) (dest *url.URL, err error) {
|
||||
|
||||
// generate a (relative) link
|
||||
var path string
|
||||
err = u.Dependencies.PHP.ExecScript(ctx, server, &path, usersPHP, "get_login_link", username)
|
||||
err = u.Dependencies.PHP.ExecScript(ctx, server, &path, usersPHP, "get_login_link", username, opts.Destination, opts.CreateIfMissing, opts.GrantAdminRole)
|
||||
|
||||
// if something went wrong, return
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -35,9 +35,21 @@ function check_password_hash($password, $hash): bool {
|
|||
return \Drupal::service('password')->check($password, $hash);
|
||||
}
|
||||
|
||||
function get_login_link($name): string {
|
||||
function get_login_link($name, $destination = "", $update_user = FALSE, $grant_admin = FALSE): string {
|
||||
$account = user_load_by_name($name);
|
||||
if (!$account) return "";
|
||||
if (!$account) {
|
||||
if (!$update_user) return "";
|
||||
$account = create_new_disabled_user($name);
|
||||
}
|
||||
|
||||
if ($update_user && $grant_admin) {
|
||||
$account->addRole('administrator');
|
||||
}
|
||||
|
||||
if ($update_user) {
|
||||
$account->save();
|
||||
}
|
||||
|
||||
|
||||
$timestamp = \Drupal::time()->getRequestTime();
|
||||
return Url::fromRoute(
|
||||
|
|
@ -49,8 +61,17 @@ function get_login_link($name): string {
|
|||
],
|
||||
[
|
||||
'absolute' => false,
|
||||
'query' => ['destination' => '/'],
|
||||
'query' => ['destination' => $destination],
|
||||
'language' => \Drupal::languageManager()->getLanguage($account->getPreferredLangcode()),
|
||||
]
|
||||
)->toString();
|
||||
}
|
||||
|
||||
function create_new_disabled_user($name) {
|
||||
$account = User::create([
|
||||
'name' => $name,
|
||||
]);
|
||||
$account->activate();
|
||||
$account->save();
|
||||
return $account;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue