From 8a5b066839b0b57315d14563f0d038a98c276b2b Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Sat, 7 Jan 2023 14:31:20 +0100 Subject: [PATCH] Expose user login functionality --- NEWS.md | 12 ++ internal/dis/component/auth/next/next.go | 109 ++++++++++++++++++ internal/dis/component/auth/panel/panel.go | 10 +- .../component/auth/panel/templates/user.html | 45 +++++++- internal/dis/component/auth/panel/user.go | 32 +++++ internal/dis/distillery.go | 2 + internal/wisski/ingredient/php/users/users.go | 17 ++- .../wisski/ingredient/php/users/users.php | 27 ++++- 8 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 internal/dis/component/auth/next/next.go diff --git a/NEWS.md b/NEWS.md index 5fedda3..287d2ea 100644 --- a/NEWS.md +++ b/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 diff --git a/internal/dis/component/auth/next/next.go b/internal/dis/component/auth/next/next.go new file mode 100644 index 0000000..515a877 --- /dev/null +++ b/internal/dis/component/auth/next/next.go @@ -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 +} diff --git a/internal/dis/component/auth/panel/panel.go b/internal/dis/component/auth/panel/panel.go index ba52d4a..5411795 100644 --- a/internal/dis/component/auth/panel/panel.go +++ b/internal/dis/component/auth/panel/panel.go @@ -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 } } diff --git a/internal/dis/component/auth/panel/templates/user.html b/internal/dis/component/auth/panel/templates/user.html index 6ec9a43..d2e71fd 100644 --- a/internal/dis/component/auth/panel/templates/user.html +++ b/internal/dis/component/auth/panel/templates/user.html @@ -55,8 +55,49 @@ {{ end }}
- There will be a list of WissKIs you have access to here. +

Your WissKIs

+

+ This is a page of WissKIs you have access to. + Click on the button containing the name to login. +

+
+
+
+
+ + + + + + + + + + {{ range $id, $grant := .Grants }} + + + + + + {{ end }} + +
+ WissKI Slug + + Drupal Username + + Admin +
+ + {{ $grant.Slug }} + + + {{ $grant.DrupalUsername }} + + {{ $grant.DrupalAdminRole }} +
+
+
- {{ end }} \ No newline at end of file diff --git a/internal/dis/component/auth/panel/user.go b/internal/dis/component/auth/panel/user.go index 878b637..9c13f9e 100644 --- a/internal/dis/component/auth/panel/user.go +++ b/internal/dis/component/auth/panel/user.go @@ -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, diff --git a/internal/dis/distillery.go b/internal/dis/distillery.go index bc0129f..2b2bea0 100644 --- a/internal/dis/distillery.go +++ b/internal/dis/distillery.go @@ -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], diff --git a/internal/wisski/ingredient/php/users/users.go b/internal/wisski/ingredient/php/users/users.go index 84966ae..d00f7f2 100644 --- a/internal/wisski/ingredient/php/users/users.go +++ b/internal/wisski/ingredient/php/users/users.go @@ -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 { diff --git a/internal/wisski/ingredient/php/users/users.php b/internal/wisski/ingredient/php/users/users.php index c07527f..95abeb4 100644 --- a/internal/wisski/ingredient/php/users/users.php +++ b/internal/wisski/ingredient/php/users/users.php @@ -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; +} \ No newline at end of file