From 8b55fd74f9b0cd0e998830b39dddea8f5bf711af Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Thu, 5 Jan 2023 12:55:28 +0100 Subject: [PATCH] Add 'dis_grant' command --- cmd/dis_grant.go | 96 +++++++++++++++++++ cmd/info.go | 5 + cmd/wdcli/main.go | 1 + internal/dis/component/auth/permission.go | 18 ++++ internal/dis/component/auth/policy/grants.go | 16 +++- internal/dis/component/auth/policy/policy.go | 4 +- internal/dis/component/auth/protect.go | 7 +- .../dis/component/auth/templates/user.html | 27 +++--- internal/dis/distillery.go | 3 + internal/status/status.go | 2 +- 10 files changed, 161 insertions(+), 18 deletions(-) create mode 100644 cmd/dis_grant.go diff --git a/cmd/dis_grant.go b/cmd/dis_grant.go new file mode 100644 index 0000000..b785459 --- /dev/null +++ b/cmd/dis_grant.go @@ -0,0 +1,96 @@ +package cmd + +import ( + wisski_distillery "github.com/FAU-CDI/wisski-distillery" + "github.com/FAU-CDI/wisski-distillery/internal/cli" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" + "github.com/FAU-CDI/wisski-distillery/internal/models" +) + +// DisGrant is the 'dis_grant' command +var DisGrant wisski_distillery.Command = disGrant{} + +type disGrant struct { + AddUser bool `short:"a" long:"add" description:"add or update a user to a given wisski"` + RemoveUser bool `short:"r" long:"remove" description:"remove a user from a given wisski"` + + DrupalAdmin bool `short:"A" long:"admin" description:"grant user the admin role"` + + Positionals struct { + User string `positional-arg-name:"USER" required:"1-1" description:"distillery username"` + Slug string `positional-arg-name:"SLUG" required:"1-1" description:"WissKI instance"` + DrupalUser string `positional-arg-name:"DRUPAL" description:"drupal username"` + } `positional-args:"true"` +} + +func (disGrant) Description() wisski_distillery.Description { + return wisski_distillery.Description{ + Requirements: cli.Requirements{ + NeedsDistillery: true, + }, + Command: "dis_grant", + Description: "grant distillery users access to specific WissKIs", + } +} + +func (dg disGrant) AfterParse() error { + var counter int + for _, action := range []bool{ + dg.AddUser, + dg.RemoveUser, + } { + if action { + counter++ + } + } + + if counter != 1 { + return errNoActionSelected + } + + return nil +} + +func (dg disGrant) Run(context wisski_distillery.Context) error { + switch { + case dg.AddUser: + return dg.runAddUser(context) + case dg.RemoveUser: + return dg.runRemoveUser(context) + } + panic("never reached") +} + +func (dg disGrant) checkHasSlug(context wisski_distillery.Context) error { + has, err := context.Environment.Instances().Has(context.Context, dg.Positionals.Slug) + if err != nil { + return err + } + if !has { + return instances.ErrWissKINotFound + } + return nil +} + +func (dg disGrant) runAddUser(context wisski_distillery.Context) error { + if err := dg.checkHasSlug(context); err != nil { + return err + } + + policy := context.Environment.Policy() + return policy.Set(context.Context, models.Grant{ + User: dg.Positionals.User, + Slug: dg.Positionals.Slug, + DrupalUsername: dg.Positionals.DrupalUser, + DrupalAdminRole: dg.DrupalAdmin, + }) +} + +func (dg disGrant) runRemoveUser(context wisski_distillery.Context) error { + if err := dg.checkHasSlug(context); err != nil { + return err + } + + policy := context.Environment.Policy() + return policy.Remove(context.Context, dg.Positionals.User, dg.Positionals.Slug) +} diff --git a/cmd/info.go b/cmd/info.go index ec3963d..cc5c5cf 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -101,5 +101,10 @@ func (i info) Run(context wisski_distillery.Context) error { context.Printf("- %v\n", user) } + context.Printf("Grants: (count %d)\n", len(info.Grants)) + for _, grant := range info.Grants { + context.Printf("- %v\n", grant) + } + return nil } diff --git a/cmd/wdcli/main.go b/cmd/wdcli/main.go index 07b9055..0db0d1c 100644 --- a/cmd/wdcli/main.go +++ b/cmd/wdcli/main.go @@ -52,6 +52,7 @@ func init() { // distillery auth wdcli.Register(cmd.DisUser) + wdcli.Register(cmd.DisGrant) // backup & cron wdcli.Register(cmd.Snapshot) diff --git a/internal/dis/component/auth/permission.go b/internal/dis/component/auth/permission.go index c279958..d3ed9a7 100644 --- a/internal/dis/component/auth/permission.go +++ b/internal/dis/component/auth/permission.go @@ -48,6 +48,24 @@ func (g grantDeny) Denied() string { return string(g) } +// AllPermissions returns a new permission that checks if all the given permissions are set +func AllPermissions(clauses ...Permission) Permission { + return func(user *AuthUser, r *http.Request) (ok Grant, err error) { + for _, clause := range clauses { + perm, err := clause.Permit(user, r) + if err != nil { + return perm, err + } + if !perm.Granted() { + return perm, nil + } + } + + // everything was fine + return grantAllow{}, nil + } +} + var errPermissionPanic = errors.New("permission: panic()") // Permit checks if the given user has this permission. diff --git a/internal/dis/component/auth/policy/grants.go b/internal/dis/component/auth/policy/grants.go index 919164f..70109c4 100644 --- a/internal/dis/component/auth/policy/grants.go +++ b/internal/dis/component/auth/policy/grants.go @@ -15,10 +15,21 @@ var ( // Set sets a specific grant, overwriting a previous grant (if any) func (policy *Policy) Set(ctx context.Context, grant models.Grant) error { - if grant.User == "" || grant.Slug == "" || grant.DrupalUsername == "" { + if grant.DrupalUsername == "" { + grant.DrupalUsername = grant.User + } + if grant.User == "" || grant.Slug == "" { return ErrInvalid } + // check that the referenced user exists! + { + _, err := policy.Dependencies.Auth.User(ctx, grant.User) + if err != nil { + return err + } + } + // get the table table, err := policy.table(ctx) if err != nil { @@ -27,8 +38,7 @@ func (policy *Policy) Set(ctx context.Context, grant models.Grant) error { // and create or update the given user / slug combination return table.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "user"}, {Name: "slug"}}, - DoUpdates: clause.AssignmentColumns([]string{"drupal_user", "admin"}), + UpdateAll: true, }).Create(&grant).Error } diff --git a/internal/dis/component/auth/policy/policy.go b/internal/dis/component/auth/policy/policy.go index 53cbd19..e16edb4 100644 --- a/internal/dis/component/auth/policy/policy.go +++ b/internal/dis/component/auth/policy/policy.go @@ -4,6 +4,7 @@ import ( "context" "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/sql" "github.com/FAU-CDI/wisski-distillery/internal/models" "gorm.io/gorm" @@ -13,7 +14,8 @@ type Policy struct { component.Base Dependencies struct { - SQL *sql.SQL + SQL *sql.SQL + Auth *auth.Auth } } diff --git a/internal/dis/component/auth/protect.go b/internal/dis/component/auth/protect.go index fc138b9..37b1cc1 100644 --- a/internal/dis/component/auth/protect.go +++ b/internal/dis/component/auth/protect.go @@ -74,5 +74,10 @@ func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler { // Admin represents a permission that checks if a user is an administrator and has totp enabled. var Admin Permission = func(user *AuthUser, r *http.Request) (ok Grant, err error) { - return Bool2Grant(user != nil && user.IsAdmin() && user.IsTOTPEnabled(), "user needs to have admin permissions and TOTP enabled"), nil + return Bool2Grant(user != nil && user.IsAdmin() && user.IsTOTPEnabled(), "user needs to have admin permissions and passcode enabled"), nil +} + +// User represents a permission that checks if a user has totp enabled. +var User Permission = func(user *AuthUser, r *http.Request) (ok Grant, err error) { + return Bool2Grant(user != nil && user.IsEnabled() && user.IsTOTPEnabled(), "user needs to have passcode enabled"), nil } diff --git a/internal/dis/component/auth/templates/user.html b/internal/dis/component/auth/templates/user.html index acb210e..4792176 100644 --- a/internal/dis/component/auth/templates/user.html +++ b/internal/dis/component/auth/templates/user.html @@ -13,23 +13,26 @@ {{ define "content" }}

- {{ if .User.IsAdmin }} - You are an administrator. - {{ else }} - You are a regular user. - {{ end }} - {{ if .User.IsTOTPEnabled }} - You have TOTP enabled. - {{ else }} - You do not have TOTP enabled. - {{ end }} +

Change Password {{ if .User.IsTOTPEnabled }} - Disable TOTP + Disable Passcode (TOTP) {{ else }} - Enable TOTP + Enable Passcode (TOTP) {{ end }}

diff --git a/internal/dis/distillery.go b/internal/dis/distillery.go index a90567d..ddc5bf0 100644 --- a/internal/dis/distillery.go +++ b/internal/dis/distillery.go @@ -103,6 +103,9 @@ func (dis *Distillery) Provisionable() []component.Provisionable { func (dis *Distillery) Info() *admin.Admin { return export[*admin.Admin](dis) } +func (dis *Distillery) Policy() *policy.Policy { + return export[*policy.Policy](dis) +} // // All components diff --git a/internal/status/status.go b/internal/status/status.go index 55a6d01..369e04e 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -1,2 +1,2 @@ -// Package status provides status information +// Package status provides dagta structutres for distillery information package status