Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Site account extensions #2008

Merged
merged 63 commits into from
Aug 20, 2021
Merged
Changes from 1 commit
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
fa71253
Add Dockerfile.old
Daniel-WWU-IT Jun 30, 2021
2012843
Restructure endpoint handling
Daniel-WWU-IT Jun 30, 2021
68639b0
Add some more fields to user accounts
Daniel-WWU-IT Jun 30, 2021
a9f422b
Add user passwords
Daniel-WWU-IT Jul 1, 2021
42ed780
Minor corrections
Daniel-WWU-IT Jul 2, 2021
b3fe4e1
Rename panel to admin
Daniel-WWU-IT Jul 2, 2021
cf8b597
Use bcrypt for storing passwords
Daniel-WWU-IT Jul 2, 2021
54b5258
Implement an HTML panel engine
Daniel-WWU-IT Jul 5, 2021
6abccff
Add user role to accounts
Daniel-WWU-IT Jul 5, 2021
e5a9f2e
Move endpoint /register to /account
Daniel-WWU-IT Jul 5, 2021
951bfe5
Add session management
Daniel-WWU-IT Jul 6, 2021
9836a6c
Support multiple templates per panel
Daniel-WWU-IT Jul 6, 2021
4174708
Add login form (WIP)
Daniel-WWU-IT Jul 7, 2021
6f89beb
Add user authentication endpoint
Daniel-WWU-IT Jul 7, 2021
a94675f
Restructure session management and support login
Daniel-WWU-IT Jul 8, 2021
5f556a3
Implement user login
Daniel-WWU-IT Jul 8, 2021
b7b8709
Work on user management template
Daniel-WWU-IT Jul 9, 2021
9768156
Add account editing
Daniel-WWU-IT Jul 12, 2021
94a4f6b
Improve session handling
Daniel-WWU-IT Jul 12, 2021
9a3b182
Enable password reset
Daniel-WWU-IT Jul 13, 2021
ed4d952
Fix storing of logged in account
Daniel-WWU-IT Jul 13, 2021
bc99b76
Merge branch 'master-upstream' into siteacc-ext
Daniel-WWU-IT Jul 13, 2021
fd81812
Remove debug output
Daniel-WWU-IT Jul 13, 2021
8c5d9c1
Fix password reset form
Daniel-WWU-IT Jul 13, 2021
035d773
Add GOCDB access flag to accounts
Daniel-WWU-IT Jul 14, 2021
f6efa98
Verify account website and phone number
Daniel-WWU-IT Jul 14, 2021
35133a5
Session handling improvements
Daniel-WWU-IT Jul 16, 2021
27516be
Session handling improvements
Daniel-WWU-IT Jul 16, 2021
f5e7a74
Debugging
Daniel-WWU-IT Jul 16, 2021
efb469e
Debugging
Daniel-WWU-IT Jul 16, 2021
a64215d
Fix URI path handling
Daniel-WWU-IT Jul 16, 2021
e1fdfb0
Fixes
Daniel-WWU-IT Jul 16, 2021
3f38706
Edit account template improvements
Daniel-WWU-IT Jul 16, 2021
6873199
Add user tokens for external authentication
Daniel-WWU-IT Jul 19, 2021
02538da
User token refreshing
Daniel-WWU-IT Jul 20, 2021
b70dc83
Add scope check to login
Daniel-WWU-IT Jul 20, 2021
a3c6e75
Make verify-user-token a GET endpoint
Daniel-WWU-IT Jul 21, 2021
0c3ebb4
Switch to JWT for user tokens
Daniel-WWU-IT Jul 22, 2021
e942d45
Form improvements
Daniel-WWU-IT Jul 23, 2021
189de96
Merge branch 'master-upstream' into siteacc-ext
Daniel-WWU-IT Jul 26, 2021
aaabc72
Email updates
Daniel-WWU-IT Jul 26, 2021
2a4467f
Add request form
Daniel-WWU-IT Jul 27, 2021
9f2d0f6
Trim form entries
Daniel-WWU-IT Jul 28, 2021
8f9b955
JS updates
Daniel-WWU-IT Jul 29, 2021
ff892a4
Make all URLs absolute
Daniel-WWU-IT Jul 29, 2021
1904a27
General improvements
Daniel-WWU-IT Jul 29, 2021
6a71f76
Typo
Daniel-WWU-IT Jul 29, 2021
c742eb7
Fix cookie settings
Daniel-WWU-IT Jul 29, 2021
f5c4819
Form improvements
Daniel-WWU-IT Aug 2, 2021
66b146b
Hash password on client side
Daniel-WWU-IT Aug 2, 2021
97d9e99
Hash password on client side
Daniel-WWU-IT Aug 2, 2021
72d8925
Revert "Hash password on client side"
Daniel-WWU-IT Aug 2, 2021
f276644
Revert "Hash password on client side"
Daniel-WWU-IT Aug 2, 2021
cb9219c
Form improvements
Daniel-WWU-IT Aug 2, 2021
0e229e6
Small form improvements
Daniel-WWU-IT Aug 3, 2021
5838fd0
Add title to accounts
Daniel-WWU-IT Aug 11, 2021
719c756
Add synchronization with GOCDB
Daniel-WWU-IT Aug 16, 2021
e22f3ed
Merge branch 'master-upstream' into siteacc-ext
Daniel-WWU-IT Aug 19, 2021
6c010bb
Use official JWT package
Daniel-WWU-IT Aug 19, 2021
05cc442
Add changelog
Daniel-WWU-IT Aug 19, 2021
b001930
Hound fixes
Daniel-WWU-IT Aug 19, 2021
9163342
Lint fixes
Daniel-WWU-IT Aug 19, 2021
2668213
Hound fixes
Daniel-WWU-IT Aug 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add account editing
Daniel-WWU-IT committed Jul 12, 2021
commit 97681563c0108ee4c1a19ed820ec615bfbdd6feb
50 changes: 50 additions & 0 deletions pkg/siteacc/account/edit/edit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package edit

import "github.com/cs3org/reva/pkg/siteacc/html"

type PanelTemplate struct {
html.ContentProvider
}

// GetTitle returns the title of the panel.
func (template *PanelTemplate) GetTitle() string {
return "ScienceMesh Account"
}

// GetCaption returns the caption which is displayed on the panel.
func (template *PanelTemplate) GetCaption() string {
return "Edit your ScienceMesh Account!"
}

// GetContentJavaScript delivers additional JavaScript code.
func (template *PanelTemplate) GetContentJavaScript() string {
return tplJavaScript
}

// GetContentStyleSheet delivers additional stylesheet code.
func (template *PanelTemplate) GetContentStyleSheet() string {
return tplStyleSheet
}

// GetContentBody delivers the actual body content.
func (template *PanelTemplate) GetContentBody() string {
return tplBody
}
161 changes: 161 additions & 0 deletions pkg/siteacc/account/edit/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package edit

const tplJavaScript = `
function verifyForm(formData) {
if (formData.get("fname") == "") {
setState(STATE_ERROR, "Please specify your first name.", "form", "fname", true);
return false;
}
if (formData.get("lname") == "") {
setState(STATE_ERROR, "Please specify your last name.", "form", "lname", true);
return false;
}
if (formData.get("organization") == "") {
setState(STATE_ERROR, "Please specify your organization/company.", "form", "organization", true);
return false;
}
if (formData.get("role") == "") {
setState(STATE_ERROR, "Please specify your role within the organization/company.", "form", "role", true);
return false;
}
if (formData.get("password") != "") {
if (formData.get("password2") == "") {
setState(STATE_ERROR, "Please confirm your new password.", "form", "password2", true);
return false;
}
if (formData.get("password") != formData.get("password2")) {
setState(STATE_ERROR, "The entered passwords do not match.", "form", "password2", true);
return false;
}
}
return true;
}
function handleAction(action) {
const formData = new FormData(document.querySelector("form"));
if (!verifyForm(formData)) {
return;
}
setState(STATE_STATUS, "Updating account... this should only take a moment.", "form", null, false);
var xhr = new XMLHttpRequest();
xhr.open("POST", action);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE) {
if (this.status == 200) {
setState(STATE_SUCCESS, "Your account was successfully updated!", "form", null, true);
} else {
var resp = JSON.parse(this.responseText);
setState(STATE_ERROR, "An error occurred while trying to update your account:<br><em>" + resp.error + "</em>", "form", null, true);
}
}
}
var postData = {
"firstName": formData.get("fname"),
"lastName": formData.get("lname"),
"organization": formData.get("organization"),
"website": formData.get("website"),
"role": formData.get("role"),
"phoneNumber": formData.get("phone"),
"password": {
"value": formData.get("password")
}
};
xhr.send(JSON.stringify(postData));
}
`

const tplStyleSheet = `
html * {
font-family: arial !important;
}
.mandatory {
color: red;
font-weight: bold;
}
`

const tplBody = `
<div>
<p>Edit your ScienceMesh account information below.</p>
<p>Please note that you cannot modify your email address using this form.</p>
</div>
<div>&nbsp;</div>
<div>
<form id="form" method="POST" class="box container-inline" style="width: 100%;">
<div style="grid-row: 1;"><label for="fname">First name: <span class="mandatory">*</span></label></div>
<div style="grid-row: 2;"><input type="text" id="fname" name="fname" value="{{.Account.FirstName}}"/></div>
<div style="grid-row: 1;"><label for="lname">Last name: <span class="mandatory">*</span></label></div>
<div style="grid-row: 2;"><input type="text" id="lname" name="lname" value="{{.Account.LastName}}"/></div>
<div style="grid-row: 3;"><label for="organization">Organization/Company: <span class="mandatory">*</span></label></div>
<div style="grid-row: 4;"><input type="text" id="organization" name="organization" value="{{.Account.Organization}}"/></div>
<div style="grid-row: 3;"><label for="website">Website:</label></div>
<div style="grid-row: 4;"><input type="text" id="website" name="website" placeholder="https://www.example.com" value="{{.Account.Website}}"/></div>
<div style="grid-row: 5;"><label for="role">Role: <span class="mandatory">*</span></label></div>
<div style="grid-row: 6;"><input type="text" id="role" name="role" placeholder="Site administrator" value="{{.Account.Role}}"/></div>
<div style="grid-row: 5;"><label for="phone">Phone number:</label></div>
<div style="grid-row: 6;"><input type="text" id="phone" name="phone" placeholder="+49 030 123456" value="{{.Account.PhoneNumber}}"/></div>
<div style="grid-row: 7;">&nbsp;</div>
<div style="grid-row: 8; grid-column: 1 / span 2;">If you want to change your password, fill out the fields below. Otherwise, leave them empty to keep your current one.</div>
<div style="grid-row: 9;"><label for="password">New password:</label></div>
<div style="grid-row: 10;"><input type="password" id="password" name="password"/></div>
<div style="grid-row: 9"><label for="password2">Confirm new password:</label></div>
<div style="grid-row: 10;"><input type="password" id="password2" name="password2"/></div>
<div style="grid-row: 11; font-style: italic; font-size: 0.8em;">
The password must fulfil the following criteria:
<ul style="margin-top: 0em;">
<li>Must be at least 8 characters long</li>
<li>Must contain at least 1 lowercase letter</li>
<li>Must contain at least 1 uppercase letter</li>
<li>Must contain at least 1 digit</li>
</ul>
</div>
<div style="grid-row: 12; align-self: center;">
Fields marked with <span class="mandatory">*</span> are mandatory.
</div>
<div style="grid-row: 12; grid-column: 2; text-align: right;">
<button type="reset">Reset</button>
<button type="button" style="font-weight: bold;" onClick="handleAction('update?invoker=user');">Save</button>
</div>
</form>
</div>
<div>
<p>Go <a href="?path=manage">back</a> to the main account page.</p>
</div>
`
2 changes: 1 addition & 1 deletion pkg/siteacc/account/manage/manage.go
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ type PanelTemplate struct {

// GetTitle returns the title of the panel.
func (template *PanelTemplate) GetTitle() string {
return "ScienceMesh Account Management"
return "ScienceMesh Account"
}

// GetCaption returns the caption which is displayed on the panel.
3 changes: 2 additions & 1 deletion pkg/siteacc/account/manage/template.go
Original file line number Diff line number Diff line change
@@ -20,7 +20,8 @@ package manage

const tplJavaScript = `
function handleEditAccount() {
setState(STATE_STATUS, "No one has implemented this function yet :(");
setState(STATE_STATUS, "Redirecting to the account editor...");
window.location.replace("?path=edit");
}
function handleRequestAccess() {
23 changes: 15 additions & 8 deletions pkg/siteacc/account/panel.go
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import (
"net/http"
"net/url"

"github.com/cs3org/reva/pkg/siteacc/account/edit"
"github.com/cs3org/reva/pkg/siteacc/account/login"
"github.com/cs3org/reva/pkg/siteacc/account/manage"
"github.com/cs3org/reva/pkg/siteacc/account/registration"
@@ -42,6 +43,7 @@ type Panel struct {
const (
templateLogin = "login"
templateManage = "manage"
templateEdit = "edit"
templateRegistration = "register"
)

@@ -62,6 +64,10 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger)
return errors.Wrap(err, "unable to create the account management template")
}

if err := panel.htmlPanel.AddTemplate(templateEdit, &edit.PanelTemplate{}); err != nil {
return errors.Wrap(err, "unable to create the account editing template")
}

if err := panel.htmlPanel.AddTemplate(templateRegistration, &registration.PanelTemplate{}); err != nil {
return errors.Wrap(err, "unable to create the registration template")
}
@@ -71,22 +77,23 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger)

// GetActiveTemplate returns the name of the active template.
func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string {
validPaths := []string{templateLogin, templateManage, templateEdit, templateRegistration}
template := templateLogin

// Invalid paths are just ignored and redirected to the login page
switch path {
case templateLogin,
templateManage,
templateRegistration:
template = path
// Only allow valid template paths; redirect to the login page otherwise
for _, valid := range validPaths {
if valid == path {
template = path
break
}
}

return template
}

// PreExecute is called before the actual template is being executed.
func (panel *Panel) PreExecute(session *html.Session, path string, w http.ResponseWriter, r *http.Request) (html.ExecutionResult, error) {
protectedPaths := []string{templateManage}
protectedPaths := []string{templateManage, templateEdit}

if session.LoggedInUser == nil {
// If no user is logged in, redirect protected paths to the login page
@@ -96,8 +103,8 @@ func (panel *Panel) PreExecute(session *html.Session, path string, w http.Respon
}
}
} else {
// If a user is logged in and tries to login or register again, redirect to the main account page
if path == templateLogin || path == templateRegistration {
// If a user is logged in and tries to login or register again, redirect to the main account page
return panel.redirect(templateManage, w, r), nil
}
}
11 changes: 9 additions & 2 deletions pkg/siteacc/data/account.go
Original file line number Diff line number Diff line change
@@ -66,8 +66,8 @@ func (acc *Account) GetSiteID() key.SiteIdentifier {
return ""
}

// Update copies the data of the given account to this account; if copyData is true, the account data is copied as well.
func (acc *Account) Update(other *Account, copyData bool) error {
// Update copies the data of the given account to this account.
func (acc *Account) Update(other *Account, setPassword bool, copyData bool) error {
if err := other.verify(false); err != nil {
return errors.Wrap(err, "unable to update account data")
}
@@ -80,6 +80,13 @@ func (acc *Account) Update(other *Account, copyData bool) error {
acc.Role = other.Role
acc.PhoneNumber = other.PhoneNumber

if setPassword && other.Password.Value != "" {
// If a password was provided, use that as the new one
if err := acc.UpdatePassword(other.Password.Value); err != nil {
return errors.Wrap(err, "unable to update account data")
}
}

if copyData {
acc.Data = other.Data
}
31 changes: 23 additions & 8 deletions pkg/siteacc/endpoints.go
Original file line number Diff line number Diff line change
@@ -33,6 +33,11 @@ import (
"github.com/pkg/errors"
)

const (
invokerDefault = ""
invokerUser = "user"
)

type methodCallback = func(*SiteAccounts, url.Values, []byte, *html.Session) (interface{}, error)

type endpoint struct {
@@ -190,7 +195,7 @@ func handleAssignAPIKey(siteacc *SiteAccounts, values url.Values, body []byte, s
flags |= key.FlagScienceMesh
}

// Assign a new API key to the account through the account accountsManager
// Assign a new API key to the account through the accounts manager
if err := siteacc.AccountsManager().AssignAPIKeyToAccount(account, flags); err != nil {
return nil, errors.Wrap(err, "unable to assign API key")
}
@@ -216,7 +221,7 @@ func handleCreate(siteacc *SiteAccounts, values url.Values, body []byte, session
return nil, err
}

// Create a new account through the account accountsManager
// Create a new account through the accounts manager
if err := siteacc.AccountsManager().CreateAccount(account); err != nil {
return nil, errors.Wrap(err, "unable to create account")
}
@@ -230,8 +235,18 @@ func handleUpdate(siteacc *SiteAccounts, values url.Values, body []byte, session
return nil, err
}

// Update the account through the account accountsManager; only the basic data of an account can be updated through this requestHandler
if err := siteacc.AccountsManager().UpdateAccount(account, false); err != nil {
invokedByUser := strings.EqualFold(values.Get("invoker"), invokerUser)
if invokedByUser {
// If this endpoint was called by the user, set the account email from the stored session
if session.LoggedInUser == nil {
return nil, errors.Errorf("no user is currently logged in")
}

account.Email = session.LoggedInUser.Email
}

// Update the account through the accounts manager; only the basic data of an account can be updated through this requestHandler
if err := siteacc.AccountsManager().UpdateAccount(account, invokedByUser, false); err != nil {
return nil, errors.Wrap(err, "unable to update account")
}

@@ -244,7 +259,7 @@ func handleRemove(siteacc *SiteAccounts, values url.Values, body []byte, session
return nil, err
}

// Remove the account through the account accountsManager
// Remove the account through the accounts manager
if err := siteacc.AccountsManager().RemoveAccount(account); err != nil {
return nil, errors.Wrap(err, "unable to remove account")
}
@@ -266,7 +281,7 @@ func handleUnregisterSite(siteacc *SiteAccounts, values url.Values, body []byte,
return nil, err
}

// Unregister the account's site through the account accountsManager
// Unregister the account's site through the accounts manager
if err := siteacc.AccountsManager().UnregisterAccountSite(account); err != nil {
return nil, errors.Wrap(err, "unable to unregister the site of the given account")
}
@@ -313,7 +328,7 @@ func handleAuthorize(siteacc *SiteAccounts, values url.Values, body []byte, sess
return nil, errors.Errorf("unsupported authorization status %v", val[0])
}

// Authorize the account through the account accountsManager
// Authorize the account through the accounts manager
if err := siteacc.AccountsManager().AuthorizeAccount(account, authorize); err != nil {
return nil, errors.Wrap(err, "unable to (un)authorize account")
}
@@ -337,7 +352,7 @@ func findAccount(siteacc *SiteAccounts, by string, value string) (*data.Account,
return nil, errors.Errorf("missing search criteria")
}

// Find the account using the account accountsManager
// Find the account using the accounts manager
account, err := siteacc.AccountsManager().FindAccount(by, value)
if err != nil {
return nil, errors.Wrap(err, "user not found")
8 changes: 5 additions & 3 deletions pkg/siteacc/html/session.go
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ import (
type Session struct {
ID string
RemoteAddress string
Timeout time.Duration
Expires time.Time

LoggedInUser *data.Account
@@ -43,9 +44,9 @@ type Session struct {
// Save stores the session ID in a cookie using a response writer.
func (sess *Session) Save(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: sess.sessionCookieName,
Value: sess.ID,
Expires: sess.Expires,
Name: sess.sessionCookieName,
Value: sess.ID,
MaxAge: int(sess.Timeout / time.Second),
})
}

@@ -76,6 +77,7 @@ func NewSession(name string, timeout time.Duration, r *http.Request) *Session {
session := &Session{
ID: uuid.NewString(),
RemoteAddress: r.RemoteAddr,
Timeout: timeout,
Expires: time.Now().Add(timeout),
Data: nil,
sessionCookieName: name,
2 changes: 2 additions & 0 deletions pkg/siteacc/html/sessionmanager.go
Original file line number Diff line number Diff line change
@@ -69,6 +69,8 @@ func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request
session = mngr.findSession(cookie.Value)
if session != nil {
// Verify the request against the session: If it is invalid, set an error; if the session has expired, migrate to a new one; otherwise, just continue
// TODO: Refresh session; new ID, new timeout
// TODO: Expired sessions are NOT restored but removed!
if err := session.VerifyRequest(r); err == nil {
if session.HasExpired() {
session, err = mngr.migrateSession(session, r)
4 changes: 2 additions & 2 deletions pkg/siteacc/manager/accmanager.go
Original file line number Diff line number Diff line change
@@ -169,7 +169,7 @@ func (mngr *AccountsManager) CreateAccount(accountData *data.Account) error {
}

// UpdateAccount updates the account identified by the account email; if no such account exists, an error is returned.
func (mngr *AccountsManager) UpdateAccount(accountData *data.Account, copyData bool) error {
func (mngr *AccountsManager) UpdateAccount(accountData *data.Account, setPassword bool, copyData bool) error {
mngr.mutex.Lock()
defer mngr.mutex.Unlock()

@@ -178,7 +178,7 @@ func (mngr *AccountsManager) UpdateAccount(accountData *data.Account, copyData b
return errors.Wrap(err, "user to update not found")
}

if err := account.Update(accountData, copyData); err == nil {
if err := account.Update(accountData, setPassword, copyData); err == nil {
account.DateModified = time.Now()

mngr.storage.AccountUpdated(account)