Initial QSfera import
This commit is contained in:
Generated
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
// 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 contact
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the contact form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Contact Form"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Contact the ScienceMesh administration!"
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Generated
Vendored
+111
@@ -0,0 +1,111 @@
|
||||
// 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 contact
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData) {
|
||||
if (formData.getTrimmed("subject") == "") {
|
||||
setState(STATE_ERROR, "Please enter a subject.", "form", "subject", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("message") == "") {
|
||||
setState(STATE_ERROR, "Please enter a message.", "form", "message", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Sending message... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your message was successfully sent! A copy of the message has been sent to your email address.");
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to send your message:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"subject": formData.getTrimmed("subject"),
|
||||
"message": formData.getTrimmed("message")
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
|
||||
.mandatory {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p>Contact the ScienceMesh administration using the form below.</p>
|
||||
<p style="margin-bottom: 0em;">Please include as much information as possible in your request, especially:</p>
|
||||
<ul style="margin-top: 0em;">
|
||||
<li>The site your request refers to (if not obvious from your account information)</li>
|
||||
<li>Your role within the ScienceMesh site (e.g., administrator, operational team member, etc.)</li>
|
||||
<li>Any specific reasons for your request</li>
|
||||
<li>Anything else that might help to process your request</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('contact'); return false;">
|
||||
<div style="grid-row: 1;"><label for="subject">Subject: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2; grid-column: 1 / span 2;"><input type="text" id="subject" name="subject" {{if .Params.Subject}}value="{{.Params.Subject}}" readonly{{end}}/></div>
|
||||
|
||||
<div style="grid-row: 3;"><label for="message">Message: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 4; grid-column: 1 / span 2;">
|
||||
<textarea rows="10" id="message" name="message" style="box-sizing: border-box; width: 100%;" {{if .Params.Info}}placeholder="{{.Params.Info}}"{{end}}>{{if .Params.Message}}{{.Params.Message}}{{end}}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 5; align-self: center;">
|
||||
Fields marked with <span class="mandatory">*</span> are mandatory.
|
||||
</div>
|
||||
<div style="grid-row: 5; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Go <a href="{{getServerAddress}}/account/?path=manage">back</a> to the main account page.</p>
|
||||
</div>
|
||||
`
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// 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/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the edit form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Account"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Edit your ScienceMesh Site Administrator 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
|
||||
}
|
||||
Generated
Vendored
+158
@@ -0,0 +1,158 @@
|
||||
// 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.getTrimmed("fname") == "") {
|
||||
setState(STATE_ERROR, "Please specify your first name.", "form", "fname", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("lname") == "") {
|
||||
setState(STATE_ERROR, "Please specify your last name.", "form", "lname", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("role") == "") {
|
||||
setState(STATE_ERROR, "Please specify your role within your site.", "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", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
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 = {
|
||||
"title": formData.getTrimmed("title"),
|
||||
"firstName": formData.getTrimmed("fname"),
|
||||
"lastName": formData.getTrimmed("lname"),
|
||||
"role": formData.getTrimmed("role"),
|
||||
"phoneNumber": formData.getTrimmed("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 Site Administrator Account information below.</p>
|
||||
<p>Please note that you cannot modify your email address using this form.</p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('update?invoker=user'); return false;">
|
||||
<div style="grid-row: 1;"><label for="title">Title: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2;">
|
||||
<select id="title" name="title">
|
||||
{{$title := .Account.Title}}
|
||||
{{range .Titles}}
|
||||
<option value="{{.}}" {{if eq . $title}}selected{{end}}>{{.}}.</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 3;"><label for="fname">First name: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 4;"><input type="text" id="fname" name="fname" value="{{.Account.FirstName}}"/></div>
|
||||
<div style="grid-row: 3;"><label for="lname">Last name: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 4;"><input type="text" id="lname" name="lname" value="{{.Account.LastName}}"/></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;"> </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" autocomplete="new-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" autocomplete="new-password"/></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="submit" style="font-weight: bold;">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Go <a href="{{getServerAddress}}/account/?path=manage">back</a> to the main account page.</p>
|
||||
</div>
|
||||
`
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// 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 login
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the login form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Account Login"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Login to your ScienceMesh Site Administrator 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
|
||||
}
|
||||
Generated
Vendored
+137
@@ -0,0 +1,137 @@
|
||||
// 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 login
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData, requirePassword = true) {
|
||||
if (formData.getTrimmed("email") == "") {
|
||||
setState(STATE_ERROR, "Please enter your email address.", "form", "email", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requirePassword) {
|
||||
if (formData.get("password") == "") {
|
||||
setState(STATE_ERROR, "Please enter your password.", "form", "password", true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Logging in... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your login was successful! Redirecting...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=manage");
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to login your account:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"email": formData.getTrimmed("email"),
|
||||
"password": {
|
||||
"value": formData.get("password")
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
|
||||
function handleResetPassword() {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Resetting password... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/reset-password");
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your password was successfully reset! Please check your inbox for your new password.", "form", null, true);
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to reset your password:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"email": formData.get("email")
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
|
||||
.mandatory {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p>Login to your ScienceMesh Site Administrator Account using the form below.</p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('login'); return false;">
|
||||
<div style="grid-row: 1;"><label for="email">Email address: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2;"><input type="text" id="email" name="email" placeholder="me@example.com"/></div>
|
||||
<div style="grid-row: 1;"><label for="password">Password: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2;"><input type="password" id="password" name="password"/></div>
|
||||
<div style="grid-row: 3; grid-column: 2; font-style: italic; font-size: 0.8em;">
|
||||
Forgot your password? Click <a href="#" onClick="handleResetPassword();">here</a> to reset it.
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 4; align-self: center;">
|
||||
Fields marked with <span class="mandatory">*</span> are mandatory.
|
||||
</div>
|
||||
<div style="grid-row: 4; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Don't' have an account yet? Register <a href="{{getServerAddress}}/account/?path=register">here</a>.</p>
|
||||
</div>
|
||||
`
|
||||
Generated
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
// 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 manage
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the mangement form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Account"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Welcome to your ScienceMesh Site Administrator 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
|
||||
}
|
||||
Generated
Vendored
+132
@@ -0,0 +1,132 @@
|
||||
// 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 manage
|
||||
|
||||
const tplJavaScript = `
|
||||
function handleAccountSettings() {
|
||||
setState(STATE_STATUS, "Redirecting to the account settings...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=settings");
|
||||
}
|
||||
|
||||
function handleEditAccount() {
|
||||
setState(STATE_STATUS, "Redirecting to the account editor...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=edit");
|
||||
}
|
||||
|
||||
function handleSiteSettings() {
|
||||
setState(STATE_STATUS, "Redirecting to the site settings...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=site");
|
||||
}
|
||||
|
||||
function handleRequestAccess(scope) {
|
||||
setState(STATE_STATUS, "Redirecting to the contact form...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=contact&subject=" + encodeURIComponent("Request " + scope + " access"));
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "{{getServerAddress}}/logout");
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
setState(STATE_STATUS, "Logging out...");
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Done! Redirecting...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=login");
|
||||
} else {
|
||||
setState(STATE_ERROR, "An error occurred while logging out: " + this.responseText);
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send();
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
button {
|
||||
min-width: 170px;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p><strong>Hello {{.Account.FirstName}} {{.Account.LastName}},</strong></p>
|
||||
<p>On this page, you can manage your ScienceMesh Site Administrator Account. This includes editing your personal information, requesting access to the GOCDB and more.</p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<strong>Personal information:</strong>
|
||||
<ul style="margin-top: 0em;">
|
||||
<li>Name: <em>{{.Account.Title}}. {{.Account.FirstName}} {{.Account.LastName}}</em></li>
|
||||
<li>Email: <em><a href="mailto:{{.Account.Email}}">{{.Account.Email}}</a></em></li>
|
||||
<li>ScienceMesh Site: <em>{{getSiteName .Account.Site false}} ({{getSiteName .Account.Site true}})</em></li>
|
||||
<li>Role: <em>{{.Account.Role}}</em></li>
|
||||
{{if .Account.PhoneNumber}}
|
||||
<li>Phone: <em>{{.Account.PhoneNumber}}</em></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Account data:</strong>
|
||||
<ul style="margin-top: 0em;">
|
||||
<li>Site access: <em>{{if .Account.Data.SiteAccess}}Granted{{else}}Not granted{{end}}</em></li>
|
||||
<li>GOCDB access: <em>{{if .Account.Data.GOCDBAccess}}Granted{{else}}Not granted{{end}}</em></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box" style="width: 100%;">
|
||||
<div>
|
||||
<button type="button" onClick="handleAccountSettings();">Account settings</button>
|
||||
<button type="button" onClick="handleEditAccount();">Edit account</button>
|
||||
<span style="width: 25px;"> </span>
|
||||
|
||||
{{if .Account.Data.SiteAccess}}
|
||||
<button type="button" onClick="handleSiteSettings();">Site settings</button>
|
||||
<span style="width: 25px;"> </span>
|
||||
{{end}}
|
||||
|
||||
<button type="button" onClick="handleLogout();" style="float: right;">Logout</button>
|
||||
</div>
|
||||
<div style="margin-top: 0.5em;">
|
||||
<button type="button" onClick="handleRequestAccess('Site');" {{if .Account.Data.SiteAccess}}disabled{{end}}>Request Site access</button>
|
||||
<button type="button" onClick="handleRequestAccess('GOCDB');" {{if .Account.Data.GOCDBAccess}}disabled{{end}}>Request GOCDB access</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div style="font-size: 90%; margin-top: 1em;">
|
||||
<div>
|
||||
<div>Notes:</div>
|
||||
<ul style="margin-top: 0em;">
|
||||
<li>The <em>Site access</em> allows you to access and modify the global configuration of your site.</li>
|
||||
<li>The <em>GOCDB access</em> allows you to log into the central database where all site metadata is stored.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div>Quick links:</div>
|
||||
<ul style="margin-top: 0em;">
|
||||
<li><a href="https://gocdb.sciencemesh.uni-muenster.de" target="_blank">Central Database (GOCDB)</a></li>
|
||||
<li><a href="https://developer.sciencemesh.io/docs/technical-documentation/central-database/" target="_blank">Central Database documentation</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
// 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 account
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/contact"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/edit"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/login"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/manage"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/registration"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/settings"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/site"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// Panel represents the account panel.
|
||||
type Panel struct {
|
||||
html.PanelProvider
|
||||
|
||||
conf *config.Configuration
|
||||
|
||||
htmlPanel *html.Panel
|
||||
}
|
||||
|
||||
const (
|
||||
templateLogin = "login"
|
||||
templateManage = "manage"
|
||||
templateSettings = "settings"
|
||||
templateEdit = "edit"
|
||||
templateSite = "site"
|
||||
templateContact = "contact"
|
||||
templateRegistration = "register"
|
||||
)
|
||||
|
||||
func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
panel.conf = conf
|
||||
|
||||
// Create the internal HTML panel
|
||||
htmlPanel, err := html.NewPanel("account-panel", panel, conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create the account panel")
|
||||
}
|
||||
panel.htmlPanel = htmlPanel
|
||||
|
||||
// Add all templates
|
||||
if err := panel.htmlPanel.AddTemplate(templateLogin, &login.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the login template")
|
||||
}
|
||||
|
||||
if err := panel.htmlPanel.AddTemplate(templateManage, &manage.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the account management template")
|
||||
}
|
||||
|
||||
if err := panel.htmlPanel.AddTemplate(templateSettings, &settings.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the account settings 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(templateSite, &site.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the site template")
|
||||
}
|
||||
|
||||
if err := panel.htmlPanel.AddTemplate(templateContact, &contact.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the contact template")
|
||||
}
|
||||
|
||||
if err := panel.htmlPanel.AddTemplate(templateRegistration, ®istration.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the registration template")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveTemplate returns the name of the active template.
|
||||
func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string {
|
||||
validPaths := []string{templateLogin, templateManage, templateSettings, templateEdit, templateSite, templateContact, templateRegistration}
|
||||
template := templateLogin
|
||||
|
||||
// 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, templateSettings, templateEdit, templateSite, templateContact}
|
||||
|
||||
if user := session.LoggedInUser(); user != nil {
|
||||
switch path {
|
||||
case templateSite:
|
||||
// If the logged in user doesn't have site access, redirect him back to the main account page
|
||||
if !user.Account.Data.SiteAccess {
|
||||
return panel.redirect(templateManage, w, r), nil
|
||||
}
|
||||
|
||||
case templateLogin:
|
||||
case 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
|
||||
}
|
||||
} else {
|
||||
// If no user is logged in, redirect protected paths to the login page
|
||||
for _, protected := range protectedPaths {
|
||||
if protected == path {
|
||||
return panel.redirect(templateLogin, w, r), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return html.ContinueExecution, nil
|
||||
}
|
||||
|
||||
// Execute generates the HTTP output of the form and writes it to the response writer.
|
||||
func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *html.Session) error {
|
||||
dataProvider := func(*html.Session) interface{} {
|
||||
flatValues := make(map[string]string, len(r.URL.Query()))
|
||||
c := cases.Title(language.Und)
|
||||
for k, v := range r.URL.Query() {
|
||||
flatValues[c.String(k)] = v[0]
|
||||
}
|
||||
|
||||
availSites, err := data.QueryAvailableSites(panel.conf.Mentix.URL, panel.conf.Mentix.DataEndpoint)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to query available sites")
|
||||
}
|
||||
|
||||
type TemplateData struct {
|
||||
Site *data.Site
|
||||
Account *data.Account
|
||||
Params map[string]string
|
||||
|
||||
Titles []string
|
||||
Sites []data.SiteInformation
|
||||
}
|
||||
|
||||
tplData := TemplateData{
|
||||
Site: nil,
|
||||
Account: nil,
|
||||
Params: flatValues,
|
||||
Titles: []string{"Mr", "Mrs", "Ms", "Prof", "Dr"},
|
||||
Sites: availSites,
|
||||
}
|
||||
if user := session.LoggedInUser(); user != nil {
|
||||
tplData.Site = panel.cloneUserSite(user.Site)
|
||||
tplData.Account = user.Account
|
||||
}
|
||||
return tplData
|
||||
}
|
||||
return panel.htmlPanel.Execute(w, r, session, dataProvider)
|
||||
}
|
||||
|
||||
func (panel *Panel) redirect(path string, w http.ResponseWriter, r *http.Request) html.ExecutionResult {
|
||||
// Check if the original (full) URI path is stored in the request header; if not, use the request URI to get the path
|
||||
fullPath := r.Header.Get("X-Replaced-Path")
|
||||
if fullPath == "" {
|
||||
uri, _ := url.Parse(r.RequestURI)
|
||||
fullPath = uri.Path
|
||||
}
|
||||
|
||||
// Modify the original request URL by replacing the path parameter
|
||||
newURL, _ := url.Parse(fullPath)
|
||||
params := newURL.Query()
|
||||
params.Del("path")
|
||||
params.Add("path", path)
|
||||
newURL.RawQuery = params.Encode()
|
||||
http.Redirect(w, r, newURL.String(), http.StatusFound)
|
||||
return html.AbortExecution
|
||||
}
|
||||
|
||||
func (panel *Panel) cloneUserSite(site *data.Site) *data.Site {
|
||||
// Clone the user's site and decrypt the credentials for the panel
|
||||
siteClone := site.Clone(true)
|
||||
id, secret, err := site.Config.TestClientCredentials.Get(panel.conf.Security.CredentialsPassphrase)
|
||||
if err == nil {
|
||||
siteClone.Config.TestClientCredentials.ID = id
|
||||
siteClone.Config.TestClientCredentials.Secret = secret
|
||||
}
|
||||
return siteClone
|
||||
}
|
||||
|
||||
// NewPanel creates a new account panel.
|
||||
func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) {
|
||||
form := &Panel{}
|
||||
if err := form.initialize(conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the account panel")
|
||||
}
|
||||
return form, nil
|
||||
}
|
||||
Generated
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
// 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 registration
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the registration form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Account Registration"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Welcome to the ScienceMesh Site Administrator Account Registration!"
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Generated
Vendored
+186
@@ -0,0 +1,186 @@
|
||||
// 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 registration
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData) {
|
||||
if (formData.getTrimmed("email") == "") {
|
||||
setState(STATE_ERROR, "Please specify your email address.", "form", "email", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("fname") == "") {
|
||||
setState(STATE_ERROR, "Please specify your first name.", "form", "fname", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("lname") == "") {
|
||||
setState(STATE_ERROR, "Please specify your last name.", "form", "lname", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("site") == "") {
|
||||
setState(STATE_ERROR, "Please select your ScienceMesh site.", "form", "site", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("role") == "") {
|
||||
setState(STATE_ERROR, "Please specify your role within your site.", "form", "role", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.get("password") == "") {
|
||||
setState(STATE_ERROR, "Please set a password.", "form", "password", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.get("password2") == "") {
|
||||
setState(STATE_ERROR, "Please confirm your 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, "Sending registration... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your registration was successful! Please check your inbox for a confirmation email. You will be redirected to the login page in a few seconds (if not, click <a href='{{getServerAddress}}/account/?path=login'>here</a>).");
|
||||
window.setTimeout(function() {
|
||||
window.location.replace("{{getServerAddress}}/account/?path=login");
|
||||
}, 3000);
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to register your account:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"email": formData.getTrimmed("email"),
|
||||
"title": formData.getTrimmed("title"),
|
||||
"firstName": formData.getTrimmed("fname"),
|
||||
"lastName": formData.getTrimmed("lname"),
|
||||
"site": formData.getTrimmed("site"),
|
||||
"role": formData.getTrimmed("role"),
|
||||
"phoneNumber": formData.getTrimmed("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>Fill out the form below to register for a ScienceMesh Site Administrator account. A confirmation email will be sent to you shortly after registration.</p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('create'); return false;">
|
||||
<div style="grid-row: 1;"><label for="email">Email address: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2;"><input type="text" id="email" name="email" placeholder="me@example.com"/></div>
|
||||
<div style="grid-row: 1;"><label for="site">ScienceMesh Site: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2;">
|
||||
<select id="site" name="site">
|
||||
{{range .Sites}}
|
||||
<option value="{{.ID}}">{{.Name}} | {{.FullName}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 3;"> </div>
|
||||
|
||||
<div style="grid-row: 4;"><label for="title">Title: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 5;">
|
||||
<select id="title" name="title">
|
||||
{{range .Titles}}
|
||||
<option value="{{.}}">{{.}}.</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 6;"><label for="fname">First name: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 7;"><input type="text" id="fname" name="fname"/></div>
|
||||
<div style="grid-row: 6;"><label for="lname">Last name: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 7;"><input type="text" id="lname" name="lname"/></div>
|
||||
|
||||
<div style="grid-row: 8;"><label for="role">Role: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 9;"><input type="text" id="role" name="role" placeholder="Site administrator"/></div>
|
||||
<div style="grid-row: 8;"><label for="phone">Phone number:</label></div>
|
||||
<div style="grid-row: 9;"><input type="text" id="phone" name="phone" placeholder="+49 030 123456"/></div>
|
||||
|
||||
<div style="grid-row: 10;"> </div>
|
||||
|
||||
<div style="grid-row: 11;"><label for="password">Password: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 12;"><input type="password" id="password" name="password"/></div>
|
||||
<div style="grid-row: 11;"><label for="password2">Confirm password: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 12;"><input type="password" id="password2" name="password2"/></div>
|
||||
|
||||
<div style="grid-row: 13; 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: 14; align-self: center;">
|
||||
Fields marked with <span class="mandatory">*</span> are mandatory.
|
||||
</div>
|
||||
<div style="grid-row: 14; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Already have an account? Login <a href="{{getServerAddress}}/account/?path=login">here</a>.</p>
|
||||
</div>
|
||||
`
|
||||
Generated
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
// 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 settings
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the edit form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Account"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Configure your ScienceMesh Site Administrator 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
|
||||
}
|
||||
Generated
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
// 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 settings
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Configuring account... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your account was successfully configured!", "form", null, true);
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to configure your account:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"settings": {
|
||||
"receiveAlerts": (formData.get("rcvAlerts") === "on")
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p>Configure your ScienceMesh Site Administrator Account below.</p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('configure?invoker=user'); return false;">
|
||||
<div style="grid-row: 1; grid-column: 1 / span 2;">
|
||||
<h3>Notification settings</h3>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 2; grid-column: 1 / span 2;">
|
||||
<input type="checkbox" id="rcvAlerts" name="rcvAlerts" value="on" checked disabled/>
|
||||
<label for="rcvAlerts" style="font-weight: normal;">Receive email notifications about site alerts <em>(mandatory; always on)</em></label>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 3; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Go <a href="{{getServerAddress}}/account/?path=manage">back</a> to the main account page.</p>
|
||||
</div>
|
||||
`
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// 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 site
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the edit form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Configuration"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Configure your ScienceMesh Site!"
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Generated
Vendored
+117
@@ -0,0 +1,117 @@
|
||||
// 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 site
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData) {
|
||||
if (formData.getTrimmed("clientID") == "") {
|
||||
setState(STATE_ERROR, "Please enter the name of the test user.", "form", "clientID", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.get("secret") == "") {
|
||||
setState(STATE_ERROR, "Please enter the password of the test user.", "form", "secret", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Configuring site... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your site was successfully configured!", "form", null, true);
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to configure your site:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"config": {
|
||||
"testClientCredentials": {
|
||||
"id": formData.getTrimmed("clientID"),
|
||||
"secret": formData.get("secret")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.mandatory {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p>Configure your ScienceMesh Site below. <em>These settings affect your entire site and not just your account.</em></p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('site-configure?invoker=user'); return false;">
|
||||
<div style="grid-row: 1; grid-column: 1 / span 2;">
|
||||
<h3>Test user settings</h3>
|
||||
<p>In order to perform automated tests on your site, a test user has to be configured below. Please note that the user <em>has to exist in your Reva instance</em>! If you do not have a user for automated tests in your instance yet, create one first.</p>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 2;"><label for="clientID">User name: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 3;"><input type="text" id="clientID" name="clientID" placeholder="User name" value="{{.Site.Config.TestClientCredentials.ID}}"/></div>
|
||||
<div style="grid-row: 2;"><label for="secret">Password: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 3;"><input type="password" id="secret" name="secret" placeholder="Password" value="{{.Site.Config.TestClientCredentials.Secret}}"/></div>
|
||||
|
||||
<div style="grid-row: 4;"> </div>
|
||||
|
||||
<div style="grid-row: 5; align-self: center;">
|
||||
Fields marked with <span class="mandatory">*</span> are mandatory.
|
||||
</div>
|
||||
<div style="grid-row: 5; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Go <a href="{{getServerAddress}}/account/?path=manage">back</a> to the main account page.</p>
|
||||
</div>
|
||||
`
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
// 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 admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Panel represents the web interface panel of the accounts service administration.
|
||||
type Panel struct {
|
||||
html.PanelProvider
|
||||
html.ContentProvider
|
||||
|
||||
htmlPanel *html.Panel
|
||||
}
|
||||
|
||||
const (
|
||||
templateMain = "main"
|
||||
)
|
||||
|
||||
func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
// Create the internal HTML panel
|
||||
htmlPanel, err := html.NewPanel("admin-panel", panel, conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create the administration panel")
|
||||
}
|
||||
panel.htmlPanel = htmlPanel
|
||||
|
||||
// Add all templates
|
||||
if err := panel.htmlPanel.AddTemplate(templateMain, panel); err != nil {
|
||||
return errors.Wrap(err, "unable to create the main template")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveTemplate returns the name of the active template.
|
||||
func (panel *Panel) GetActiveTemplate(*html.Session, string) string {
|
||||
return templateMain
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the htmlPanel.
|
||||
func (panel *Panel) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Accounts Panel"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the htmlPanel.
|
||||
func (panel *Panel) GetCaption() string {
|
||||
return "ScienceMesh Site Administrator Accounts ({{.Accounts | len}})"
|
||||
}
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
func (panel *Panel) GetContentJavaScript() string {
|
||||
return tplJavaScript
|
||||
}
|
||||
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
func (panel *Panel) GetContentStyleSheet() string {
|
||||
return tplStyleSheet
|
||||
}
|
||||
|
||||
// GetContentBody delivers the actual body content.
|
||||
func (panel *Panel) GetContentBody() string {
|
||||
return tplBody
|
||||
}
|
||||
|
||||
// PreExecute is called before the actual template is being executed.
|
||||
func (panel *Panel) PreExecute(*html.Session, string, http.ResponseWriter, *http.Request) (html.ExecutionResult, error) {
|
||||
return html.ContinueExecution, nil
|
||||
}
|
||||
|
||||
// Execute generates the HTTP output of the htmlPanel and writes it to the response writer.
|
||||
func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *html.Session, accounts *data.Accounts) error {
|
||||
dataProvider := func(*html.Session) interface{} {
|
||||
type TemplateData struct {
|
||||
Accounts *data.Accounts
|
||||
}
|
||||
|
||||
return TemplateData{
|
||||
Accounts: accounts,
|
||||
}
|
||||
}
|
||||
return panel.htmlPanel.Execute(w, r, session, dataProvider)
|
||||
}
|
||||
|
||||
// NewPanel creates a new administration panel.
|
||||
func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) {
|
||||
panel := &Panel{}
|
||||
if err := panel.initialize(conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the administration panel")
|
||||
}
|
||||
return panel, nil
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// 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 admin
|
||||
|
||||
const tplJavaScript = `
|
||||
function handleAction(action, email) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
setState(STATE_STATUS, "Performing request...");
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Done! Reloading...");
|
||||
location.reload();
|
||||
} else {
|
||||
setState(STATE_ERROR, "An error occurred while performing the request: " + this.responseText);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"email": email,
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div style="font-size: 14px;">
|
||||
<ul>
|
||||
{{range .Accounts}}
|
||||
<li>
|
||||
<div>
|
||||
<div>
|
||||
<strong>{{.Email}}</strong><br>
|
||||
{{.Title}}. {{.FirstName}} {{.LastName}} <em>(Joined: {{.DateCreated.Format "Jan 02, 2006 15:04"}}; Last modified: {{.DateModified.Format "Jan 02, 2006 15:04"}})</em>
|
||||
</div>
|
||||
<div>
|
||||
<ul style="padding-left: 1em;">
|
||||
<li>ScienceMesh Site: {{getSiteName .Site false}} ({{getSiteName .Site true}})</li>
|
||||
<li>Role: {{.Role}}</li>
|
||||
<li>Phone: {{.PhoneNumber}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div> </div>
|
||||
|
||||
<div>
|
||||
<strong>Account data:</strong>
|
||||
<ul style="padding-left: 1em; padding-top: 0em;">
|
||||
<li>Site access: <em>{{if .Data.SiteAccess}}Granted{{else}}Not granted{{end}}</em></li>
|
||||
<li>GOCDB access: <em>{{if .Data.GOCDBAccess}}Granted{{else}}Not granted{{end}}</em></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div> </div>
|
||||
|
||||
<div>
|
||||
<form method="POST" style="width: 100%;">
|
||||
{{if .Data.SiteAccess}}
|
||||
<button type="button" onClick="handleAction('grant-site-access?status=false', '{{.Email}}');">Revoke Site access</button>
|
||||
{{else}}
|
||||
<button type="button" onClick="handleAction('grant-site-access?status=true', '{{.Email}}');">Grant Site access</button>
|
||||
{{end}}
|
||||
|
||||
{{if .Data.GOCDBAccess}}
|
||||
<button type="button" onClick="handleAction('grant-gocdb-access?status=false', '{{.Email}}');">Revoke GOCDB access</button>
|
||||
{{else}}
|
||||
<button type="button" onClick="handleAction('grant-gocdb-access?status=true', '{{.Email}}');">Grant GOCDB access</button>
|
||||
{{end}}
|
||||
|
||||
<span style="width: 25px;"> </span>
|
||||
<button type="button" onClick="handleAction('remove', '{{.Email}}');" style="float: right;">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
<hr>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
`
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// 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 alerting
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/email"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/smtpclient"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Dispatcher is used to dispatch Prometheus alerts via email.
|
||||
type Dispatcher struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
smtp *smtpclient.SMTPCredentials
|
||||
}
|
||||
|
||||
func (dispatcher *Dispatcher) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
dispatcher.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
dispatcher.log = log
|
||||
|
||||
// Create the SMTP client
|
||||
if conf.Email.SMTP != nil {
|
||||
dispatcher.smtp = smtpclient.NewSMTPCredentials(conf.Email.SMTP)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DispatchAlerts sends the provided alert(s) via email to the appropriate recipients.
|
||||
func (dispatcher *Dispatcher) DispatchAlerts(alerts *template.Data, accounts data.Accounts) error {
|
||||
for _, alert := range alerts.Alerts {
|
||||
siteID, ok := alert.Labels["site_id"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Dispatch the alert to all accounts configured to receive it
|
||||
for _, account := range accounts {
|
||||
if strings.EqualFold(account.Site, siteID) /* && account.Settings.ReceiveAlerts */ { // TODO: Uncomment if alert notifications aren't mandatory anymore
|
||||
if err := dispatcher.dispatchAlert(alert, account); err != nil {
|
||||
// Log errors only
|
||||
dispatcher.log.Err(err).Str("id", alert.Fingerprint).Str("recipient", account.Email).Msg("unable to dispatch alert to user")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the alert to the global receiver (if set)
|
||||
if dispatcher.conf.Email.NotificationsMail != "" {
|
||||
globalAccount := data.Account{ // On-the-fly account representing the "global alerts receiver"
|
||||
Email: dispatcher.conf.Email.NotificationsMail,
|
||||
FirstName: "ScienceMesh",
|
||||
LastName: "Global Alerts receiver",
|
||||
Site: "Global",
|
||||
Role: "Alerts receiver",
|
||||
Settings: data.AccountSettings{
|
||||
ReceiveAlerts: true,
|
||||
},
|
||||
}
|
||||
if err := dispatcher.dispatchAlert(alert, &globalAccount); err != nil {
|
||||
dispatcher.log.Err(err).Str("id", alert.Fingerprint).Str("recipient", globalAccount.Email).Msg("unable to dispatch alert to global alerts receiver")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dispatcher *Dispatcher) dispatchAlert(alert template.Alert, account *data.Account) error {
|
||||
alertValues := map[string]string{
|
||||
"Status": alert.Status,
|
||||
"StartDate": alert.StartsAt.String(),
|
||||
"EndDate": alert.EndsAt.String(),
|
||||
"Fingerprint": alert.Fingerprint,
|
||||
|
||||
"Name": alert.Labels["alertname"],
|
||||
"Service": alert.Labels["service_type"],
|
||||
"Instance": alert.Labels["instance"],
|
||||
"Job": alert.Labels["job"],
|
||||
"Severity": alert.Labels["severity"],
|
||||
"Site": alert.Labels["site"],
|
||||
"SiteID": alert.Labels["site_id"],
|
||||
|
||||
"Description": alert.Annotations["description"],
|
||||
"Summary": alert.Annotations["summary"],
|
||||
}
|
||||
|
||||
return email.SendAlertNotification(account, []string{account.Email}, alertValues, *dispatcher.conf)
|
||||
}
|
||||
|
||||
// NewDispatcher creates a new dispatcher instance.
|
||||
func NewDispatcher(conf *config.Configuration, log *zerolog.Logger) (*Dispatcher, error) {
|
||||
dispatcher := &Dispatcher{}
|
||||
if err := dispatcher.initialize(conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the alerts dispatcher")
|
||||
}
|
||||
return dispatcher, nil
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/smtpclient"
|
||||
)
|
||||
|
||||
// Configuration holds the general service configuration.
|
||||
type Configuration struct {
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
|
||||
Security struct {
|
||||
CredentialsPassphrase string `mapstructure:"creds_passphrase"`
|
||||
} `mapstructure:"security"`
|
||||
|
||||
Storage struct {
|
||||
Driver string `mapstructure:"driver"`
|
||||
|
||||
File struct {
|
||||
SitesFile string `mapstructure:"sites_file"`
|
||||
AccountsFile string `mapstructure:"accounts_file"`
|
||||
} `mapstructure:"file"`
|
||||
} `mapstructure:"storage"`
|
||||
|
||||
Email struct {
|
||||
SMTP *smtpclient.SMTPCredentials `mapstructure:"smtp"`
|
||||
NotificationsMail string `mapstructure:"notifications_mail"`
|
||||
} `mapstructure:"email"`
|
||||
|
||||
Mentix struct {
|
||||
URL string `mapstructure:"url"`
|
||||
DataEndpoint string `mapstructure:"data_endpoint"`
|
||||
SiteRegistrationEndpoint string `mapstructure:"sitereg_endpoint"`
|
||||
} `mapstructure:"mentix"`
|
||||
|
||||
Webserver struct {
|
||||
URL string `mapstructure:"url"`
|
||||
|
||||
SessionTimeout int `mapstructure:"session_timeout"`
|
||||
VerifyRemoteAddress bool `mapstructure:"verify_remote_address"`
|
||||
LogSessions bool `mapstructure:"log_sessions"`
|
||||
} `mapstructure:"webserver"`
|
||||
|
||||
GOCDB struct {
|
||||
URL string `mapstructure:"url"`
|
||||
WriteURL string `mapstructure:"write_url"`
|
||||
|
||||
APIKey string `mapstructure:"apikey"`
|
||||
} `mapstructure:"gocdb"`
|
||||
}
|
||||
|
||||
// Cleanup cleans up certain settings, normalizing them.
|
||||
func (cfg *Configuration) Cleanup() {
|
||||
// Ensure the webserver URL ends with a slash
|
||||
if cfg.Webserver.URL != "" && !strings.HasSuffix(cfg.Webserver.URL, "/") {
|
||||
cfg.Webserver.URL += "/"
|
||||
}
|
||||
|
||||
// Ensure the GOCDB URL ends with a slash
|
||||
if cfg.GOCDB.URL != "" && !strings.HasSuffix(cfg.GOCDB.URL, "/") {
|
||||
cfg.GOCDB.URL += "/"
|
||||
}
|
||||
|
||||
// Ensure the GOCDB Write URL ends with a slash
|
||||
if cfg.GOCDB.WriteURL != "" && !strings.HasSuffix(cfg.GOCDB.WriteURL, "/") {
|
||||
cfg.GOCDB.WriteURL += "/"
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// 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 config
|
||||
|
||||
const (
|
||||
// EndpointAdministration is the endpoint path of the web interface administration panel.
|
||||
EndpointAdministration = "/admin"
|
||||
// EndpointAccount is the endpoint path of the web interface account panel.
|
||||
EndpointAccount = "/account"
|
||||
|
||||
// EndpointList is the endpoint path for listing all stored accounts.
|
||||
EndpointList = "/list"
|
||||
// EndpointFind is the endpoint path for finding accounts.
|
||||
EndpointFind = "/find"
|
||||
|
||||
// EndpointCreate is the endpoint path for account creation.
|
||||
EndpointCreate = "/create"
|
||||
// EndpointUpdate is the endpoint path for account updates.
|
||||
EndpointUpdate = "/update"
|
||||
// EndpointConfigure is the endpoint path for account configuration.
|
||||
EndpointConfigure = "/configure"
|
||||
// EndpointRemove is the endpoint path for account removal.
|
||||
EndpointRemove = "/remove"
|
||||
|
||||
// EndpointSiteGet is the endpoint path for retrieving site data.
|
||||
EndpointSiteGet = "/site-get"
|
||||
// EndpointSiteConfigure is the endpoint path for site configuration.
|
||||
EndpointSiteConfigure = "/site-configure"
|
||||
|
||||
// EndpointLogin is the endpoint path for (internal) user login.
|
||||
EndpointLogin = "/login"
|
||||
// EndpointLogout is the endpoint path for (internal) user logout.
|
||||
EndpointLogout = "/logout"
|
||||
// EndpointResetPassword is the endpoint path for resetting user passwords
|
||||
EndpointResetPassword = "/reset-password"
|
||||
// EndpointContact is the endpoint path for sending contact emails
|
||||
EndpointContact = "/contact"
|
||||
|
||||
// EndpointVerifyUserToken is the endpoint path for user token validation.
|
||||
EndpointVerifyUserToken = "/verify-user-token"
|
||||
|
||||
// EndpointGrantSiteAccess is the endpoint path for granting or revoking Site access.
|
||||
EndpointGrantSiteAccess = "/grant-site-access"
|
||||
// EndpointGrantGOCDBAccess is the endpoint path for granting or revoking GOCDB access.
|
||||
EndpointGrantGOCDBAccess = "/grant-gocdb-access"
|
||||
|
||||
// EndpointDispatchAlert is the endpoint path for dispatching alerts from Prometheus.
|
||||
EndpointDispatchAlert = "/dispatch-alert"
|
||||
)
|
||||
Generated
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
// 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 credentials
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/credentials/crypto"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Credentials stores and en-/decrypts credentials
|
||||
type Credentials struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// Get decrypts and retrieves the stored credentials.
|
||||
func (creds *Credentials) Get(passphrase string) (string, string, error) {
|
||||
id, err := crypto.DecodeString(creds.ID, passphrase)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "unable to decode ID")
|
||||
}
|
||||
secret, err := crypto.DecodeString(creds.Secret, passphrase)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "unable to decode secret")
|
||||
}
|
||||
return id, secret, nil
|
||||
}
|
||||
|
||||
// Set encrypts and sets new credentials.
|
||||
func (creds *Credentials) Set(id, secret string, passphrase string) error {
|
||||
if s, err := crypto.EncodeString(id, passphrase); err == nil {
|
||||
creds.ID = s
|
||||
} else {
|
||||
return errors.Wrap(err, "unable to encode ID")
|
||||
}
|
||||
if s, err := crypto.EncodeString(secret, passphrase); err == nil {
|
||||
creds.Secret = s
|
||||
} else {
|
||||
return errors.Wrap(err, "unable to encode secret")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValid checks whether the credentials are valid.
|
||||
func (creds *Credentials) IsValid() bool {
|
||||
return len(creds.ID) > 0 && len(creds.Secret) > 0
|
||||
}
|
||||
|
||||
// Clear resets the credentials.
|
||||
func (creds *Credentials) Clear() {
|
||||
creds.ID = ""
|
||||
creds.Secret = ""
|
||||
}
|
||||
Generated
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
// 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 crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
passphraseLength = 32
|
||||
)
|
||||
|
||||
// EncodeString encodes a string using AES and returns the base64-encoded result.
|
||||
func EncodeString(s string, passphrase string) (string, error) {
|
||||
if len(s) == 0 || len(passphrase) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
passphrase = normalizePassphrase(passphrase)
|
||||
|
||||
gcm, err := createGCM([]byte(passphrase))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", errors.Wrap(err, "unable to generate nonce")
|
||||
}
|
||||
encryptedData := gcm.Seal(nonce, nonce, []byte(s), nil)
|
||||
return base64.StdEncoding.EncodeToString(encryptedData), nil
|
||||
}
|
||||
|
||||
// DecodeString decodes a base64-encoded string encoded with AES.
|
||||
func DecodeString(s string, passphrase string) (string, error) {
|
||||
if len(s) == 0 || len(passphrase) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
data, _ := base64.StdEncoding.DecodeString(s)
|
||||
passphrase = normalizePassphrase(passphrase)
|
||||
|
||||
gcm, err := createGCM([]byte(passphrase))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(s) < nonceSize {
|
||||
return "", errors.Errorf("input string length too short")
|
||||
}
|
||||
nonce, data := data[:nonceSize], data[nonceSize:]
|
||||
plain, err := gcm.Open(nil, nonce, data, nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to decode string")
|
||||
}
|
||||
return string(plain), nil
|
||||
}
|
||||
|
||||
func createGCM(passphrase []byte) (cipher.AEAD, error) {
|
||||
c, err := aes.NewCipher(passphrase)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to generate cipher")
|
||||
}
|
||||
gcm, err := cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to generate GCM")
|
||||
}
|
||||
return gcm, nil
|
||||
}
|
||||
|
||||
func normalizePassphrase(passphrase string) string {
|
||||
if len(passphrase) > passphraseLength {
|
||||
passphrase = passphrase[:passphraseLength]
|
||||
} else if len(passphrase) < passphraseLength {
|
||||
for i := len(passphrase); i < passphraseLength; i++ {
|
||||
passphrase += "#"
|
||||
}
|
||||
}
|
||||
return passphrase
|
||||
}
|
||||
Generated
Vendored
+83
@@ -0,0 +1,83 @@
|
||||
// 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 credentials
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Password holds a hash password alongside its salt value.
|
||||
type Password struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
const (
|
||||
passwordMinLength = 8
|
||||
)
|
||||
|
||||
// Set sets a new password by hashing the plaintext version using bcrypt.
|
||||
func (password *Password) Set(pwd string) error {
|
||||
if err := VerifyPassword(pwd); err != nil {
|
||||
return errors.Wrap(err, "invalid password")
|
||||
}
|
||||
|
||||
pwdData, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to generate password hash")
|
||||
}
|
||||
password.Value = string(pwdData)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compare checks whether the given password string equals the stored one.
|
||||
func (password *Password) Compare(pwd string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(password.Value), []byte(pwd)) == nil
|
||||
}
|
||||
|
||||
// IsValid checks whether the password is valid.
|
||||
func (password *Password) IsValid() bool {
|
||||
// bcrypt hashes are in the form of $[version]$[cost]$[22 character salt][31 character hash], so they have a minimum length of 58
|
||||
return len(password.Value) > 58 && strings.Count(password.Value, "$") >= 3
|
||||
}
|
||||
|
||||
// Clear resets the password.
|
||||
func (password *Password) Clear() {
|
||||
password.Value = ""
|
||||
}
|
||||
|
||||
// VerifyPassword checks whether the given password abides to the enforced password strength.
|
||||
func VerifyPassword(pwd string) error {
|
||||
if len(pwd) < passwordMinLength {
|
||||
return errors.Errorf("the password must be at least 8 characters long")
|
||||
}
|
||||
if !strings.ContainsAny(pwd, "abcdefghijklmnopqrstuvwxyz") {
|
||||
return errors.Errorf("the password must contain at least one lowercase letter")
|
||||
}
|
||||
if !strings.ContainsAny(pwd, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
|
||||
return errors.Errorf("the password must contain at least one uppercase letter")
|
||||
}
|
||||
if !strings.ContainsAny(pwd, "0123456789") {
|
||||
return errors.Errorf("the password must contain at least one digit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this filePath 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 data
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/credentials"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// Account represents a single site account.
|
||||
type Account struct {
|
||||
Email string `json:"email"`
|
||||
Title string `json:"title"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Site string `json:"site"`
|
||||
Role string `json:"role"`
|
||||
PhoneNumber string `json:"phoneNumber"`
|
||||
|
||||
Password credentials.Password `json:"password"`
|
||||
|
||||
DateCreated time.Time `json:"dateCreated"`
|
||||
DateModified time.Time `json:"dateModified"`
|
||||
|
||||
Data AccountData `json:"data"`
|
||||
Settings AccountSettings `json:"settings"`
|
||||
}
|
||||
|
||||
// AccountData holds additional data for a site account.
|
||||
type AccountData struct {
|
||||
GOCDBAccess bool `json:"gocdbAccess"`
|
||||
SiteAccess bool `json:"siteAccess"`
|
||||
}
|
||||
|
||||
// AccountSettings holds additional settings for a site account.
|
||||
type AccountSettings struct {
|
||||
ReceiveAlerts bool `json:"receiveAlerts"`
|
||||
}
|
||||
|
||||
// Accounts holds an array of site accounts.
|
||||
type Accounts = []*Account
|
||||
|
||||
// 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, false); err != nil {
|
||||
return errors.Wrap(err, "unable to update account data")
|
||||
}
|
||||
|
||||
// Manually update fields
|
||||
acc.Title = other.Title
|
||||
acc.FirstName = other.FirstName
|
||||
acc.LastName = other.LastName
|
||||
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
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Configure copies the settings of the given account to this account.
|
||||
func (acc *Account) Configure(other *Account) error {
|
||||
// Simply copy the stored settings
|
||||
acc.Settings = other.Settings
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePassword assigns a new password to the account, hashing it first.
|
||||
func (acc *Account) UpdatePassword(pwd string) error {
|
||||
if err := acc.Password.Set(pwd); err != nil {
|
||||
return errors.Wrap(err, "unable to update the user password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone creates a copy of the account; if erasePassword is set to true, the password will be cleared in the cloned object.
|
||||
func (acc *Account) Clone(erasePassword bool) *Account {
|
||||
clone := *acc
|
||||
|
||||
if erasePassword {
|
||||
clone.Password.Clear()
|
||||
}
|
||||
|
||||
return &clone
|
||||
}
|
||||
|
||||
// CheckScopeAccess checks whether the user can access the specified scope.
|
||||
func (acc *Account) CheckScopeAccess(scope string) bool {
|
||||
hasAccess := false
|
||||
|
||||
switch strings.ToLower(scope) {
|
||||
case ScopeDefault:
|
||||
hasAccess = true
|
||||
|
||||
case ScopeGOCDB:
|
||||
hasAccess = acc.Data.GOCDBAccess
|
||||
|
||||
case ScopeSite:
|
||||
hasAccess = acc.Data.SiteAccess
|
||||
}
|
||||
|
||||
return hasAccess
|
||||
}
|
||||
|
||||
// Cleanup trims all string entries.
|
||||
func (acc *Account) Cleanup() {
|
||||
acc.Email = strings.TrimSpace(acc.Email)
|
||||
acc.Title = strings.TrimSpace(acc.Title)
|
||||
acc.FirstName = strings.TrimSpace(acc.FirstName)
|
||||
acc.LastName = strings.TrimSpace(acc.LastName)
|
||||
acc.Site = strings.TrimSpace(acc.Site)
|
||||
acc.Role = strings.TrimSpace(acc.Role)
|
||||
acc.PhoneNumber = strings.TrimSpace(acc.PhoneNumber)
|
||||
}
|
||||
|
||||
func (acc *Account) verify(isNewAccount, verifyPassword bool) error {
|
||||
if acc.Email == "" {
|
||||
return errors.Errorf("no email address provided")
|
||||
} else if !utils.IsEmailValid(acc.Email) {
|
||||
return errors.Errorf("invalid email address: %v", acc.Email)
|
||||
}
|
||||
|
||||
if acc.FirstName == "" {
|
||||
return errors.Errorf("no first name provided")
|
||||
} else if !utils.IsValidName(acc.FirstName) {
|
||||
return errors.Errorf("first name contains invalid characters: %v", acc.FirstName)
|
||||
}
|
||||
|
||||
if acc.LastName == "" {
|
||||
return errors.Errorf("no last name provided")
|
||||
} else if !utils.IsValidName(acc.LastName) {
|
||||
return errors.Errorf("last name contains invalid characters: %v", acc.LastName)
|
||||
}
|
||||
|
||||
if isNewAccount && acc.Site == "" {
|
||||
return errors.Errorf("no site provided")
|
||||
}
|
||||
|
||||
if acc.Role == "" {
|
||||
return errors.Errorf("no role provided")
|
||||
} else if !utils.IsValidName(acc.Role) {
|
||||
return errors.Errorf("role contains invalid characters: %v", acc.Role)
|
||||
}
|
||||
|
||||
if acc.PhoneNumber != "" && !utils.IsValidPhoneNumber(acc.PhoneNumber) {
|
||||
return errors.Errorf("invalid phone number provided")
|
||||
}
|
||||
|
||||
if verifyPassword {
|
||||
if !acc.Password.IsValid() {
|
||||
return errors.Errorf("no valid password set")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewAccount creates a new site account.
|
||||
func NewAccount(email string, title, firstName, lastName string, site, role string, phoneNumber string, password string) (*Account, error) {
|
||||
t := time.Now()
|
||||
|
||||
acc := &Account{
|
||||
Email: email,
|
||||
Title: title,
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Site: site,
|
||||
Role: role,
|
||||
PhoneNumber: phoneNumber,
|
||||
DateCreated: t,
|
||||
DateModified: t,
|
||||
Data: AccountData{
|
||||
GOCDBAccess: false,
|
||||
SiteAccess: false,
|
||||
},
|
||||
Settings: AccountSettings{
|
||||
ReceiveAlerts: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Set the user password, which also makes sure that the given password is strong enough
|
||||
if err := acc.UpdatePassword(password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := acc.verify(true, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return acc, nil
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this filePath 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 data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// FileStorage implements a filePath-based storage.
|
||||
type FileStorage struct {
|
||||
Storage
|
||||
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
sitesFilePath string
|
||||
accountsFilePath string
|
||||
}
|
||||
|
||||
func (storage *FileStorage) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
storage.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
storage.log = log
|
||||
|
||||
if conf.Storage.File.SitesFile == "" {
|
||||
return errors.Errorf("no sites file set in the configuration")
|
||||
}
|
||||
storage.sitesFilePath = conf.Storage.File.SitesFile
|
||||
|
||||
if conf.Storage.File.AccountsFile == "" {
|
||||
return errors.Errorf("no accounts file set in the configuration")
|
||||
}
|
||||
storage.accountsFilePath = conf.Storage.File.AccountsFile
|
||||
|
||||
// Create the file directories if necessary
|
||||
_ = os.MkdirAll(filepath.Dir(storage.sitesFilePath), 0755)
|
||||
_ = os.MkdirAll(filepath.Dir(storage.accountsFilePath), 0755)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *FileStorage) readData(file string, obj interface{}) error {
|
||||
// Read the data from the specified file
|
||||
jsonData, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to read file %v", file)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonData, obj); err != nil {
|
||||
return errors.Wrapf(err, "invalid file %v", file)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadSites reads all stored sites into the given data object.
|
||||
func (storage *FileStorage) ReadSites() (*Sites, error) {
|
||||
sites := &Sites{}
|
||||
if err := storage.readData(storage.sitesFilePath, sites); err != nil {
|
||||
return nil, errors.Wrap(err, "error reading sites")
|
||||
}
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
// ReadAccounts reads all stored accounts into the given data object.
|
||||
func (storage *FileStorage) ReadAccounts() (*Accounts, error) {
|
||||
accounts := &Accounts{}
|
||||
if err := storage.readData(storage.accountsFilePath, accounts); err != nil {
|
||||
return nil, errors.Wrap(err, "error reading accounts")
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (storage *FileStorage) writeData(file string, obj interface{}) error {
|
||||
// Write the data to the specified file
|
||||
jsonData, _ := json.MarshalIndent(obj, "", "\t")
|
||||
if err := os.WriteFile(file, jsonData, 0755); err != nil {
|
||||
return errors.Wrapf(err, "unable to write file %v", file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteSites writes all stored sites from the given data object.
|
||||
func (storage *FileStorage) WriteSites(sites *Sites) error {
|
||||
if err := storage.writeData(storage.sitesFilePath, sites); err != nil {
|
||||
return errors.Wrap(err, "error writing sites")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteAccounts writes all stored accounts from the given data object.
|
||||
func (storage *FileStorage) WriteAccounts(accounts *Accounts) error {
|
||||
if err := storage.writeData(storage.accountsFilePath, accounts); err != nil {
|
||||
return errors.Wrap(err, "error writing accounts")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SiteAdded is called when a site has been added.
|
||||
func (storage *FileStorage) SiteAdded(site *Site) {
|
||||
// Simply skip this action; all data is saved solely in WriteSites
|
||||
}
|
||||
|
||||
// SiteUpdated is called when a site has been updated.
|
||||
func (storage *FileStorage) SiteUpdated(site *Site) {
|
||||
// Simply skip this action; all data is saved solely in WriteSites
|
||||
}
|
||||
|
||||
// SiteRemoved is called when a site has been removed.
|
||||
func (storage *FileStorage) SiteRemoved(site *Site) {
|
||||
// Simply skip this action; all data is saved solely in WriteSites
|
||||
}
|
||||
|
||||
// AccountAdded is called when an account has been added.
|
||||
func (storage *FileStorage) AccountAdded(account *Account) {
|
||||
// Simply skip this action; all data is saved solely in WriteAccounts
|
||||
}
|
||||
|
||||
// AccountUpdated is called when an account has been updated.
|
||||
func (storage *FileStorage) AccountUpdated(account *Account) {
|
||||
// Simply skip this action; all data is saved solely in WriteAccounts
|
||||
}
|
||||
|
||||
// AccountRemoved is called when an account has been removed.
|
||||
func (storage *FileStorage) AccountRemoved(account *Account) {
|
||||
// Simply skip this action; all data is saved solely in WriteAccounts
|
||||
}
|
||||
|
||||
// NewFileStorage creates a new file storage.
|
||||
func NewFileStorage(conf *config.Configuration, log *zerolog.Logger) (*FileStorage, error) {
|
||||
storage := &FileStorage{}
|
||||
if err := storage.initialize(conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the file storage")
|
||||
}
|
||||
return storage, nil
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// 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 data
|
||||
|
||||
const (
|
||||
// ScopeDefault is the default account panel scope.
|
||||
ScopeDefault = ""
|
||||
// ScopeGOCDB is used to access the GOCDB.
|
||||
ScopeGOCDB = "gocdb"
|
||||
// ScopeSite is used to access the global site configuration.
|
||||
ScopeSite = "site"
|
||||
)
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// 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 data
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/credentials"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Site represents the global site-specific settings stored in the service.
|
||||
type Site struct {
|
||||
ID string `json:"id"`
|
||||
|
||||
Config SiteConfiguration `json:"config"`
|
||||
}
|
||||
|
||||
// SiteConfiguration stores the global configuration of a site.
|
||||
type SiteConfiguration struct {
|
||||
TestClientCredentials credentials.Credentials `json:"testClientCredentials"`
|
||||
}
|
||||
|
||||
// Sites holds an array of sites.
|
||||
type Sites = []*Site
|
||||
|
||||
// Update copies the data of the given site to this site.
|
||||
func (site *Site) Update(other *Site, credsPassphrase string) error {
|
||||
if other.Config.TestClientCredentials.IsValid() {
|
||||
// If credentials were provided, use those as the new ones
|
||||
if err := site.UpdateTestClientCredentials(other.Config.TestClientCredentials.ID, other.Config.TestClientCredentials.Secret, credsPassphrase); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTestClientCredentials assigns new test client credentials, encrypting the information first.
|
||||
func (site *Site) UpdateTestClientCredentials(id, secret string, passphrase string) error {
|
||||
if err := site.Config.TestClientCredentials.Set(id, secret, passphrase); err != nil {
|
||||
return errors.Wrap(err, "unable to update the test client credentials")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone creates a copy of the site; if eraseCredentials is set to true, the (test user) credentials will be cleared in the cloned object.
|
||||
func (site *Site) Clone(eraseCredentials bool) *Site {
|
||||
clone := *site
|
||||
|
||||
if eraseCredentials {
|
||||
clone.Config.TestClientCredentials.Clear()
|
||||
}
|
||||
|
||||
return &clone
|
||||
}
|
||||
|
||||
// NewSite creates a new site.
|
||||
func NewSite(id string) (*Site, error) {
|
||||
site := &Site{
|
||||
ID: id,
|
||||
Config: SiteConfiguration{
|
||||
TestClientCredentials: credentials.Credentials{},
|
||||
},
|
||||
}
|
||||
return site, nil
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
// 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 data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/mentix/utils/network"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// SiteInformation holds the most basic information about a site.
|
||||
type SiteInformation struct {
|
||||
ID string
|
||||
Name string
|
||||
FullName string
|
||||
}
|
||||
|
||||
// QueryAvailableSites uses Mentix to query a list of all available (registered) sites.
|
||||
func QueryAvailableSites(mentixHost, dataEndpoint string) ([]SiteInformation, error) {
|
||||
mentixURL, err := network.GenerateURL(mentixHost, dataEndpoint, network.URLParams{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to generate Mentix URL")
|
||||
}
|
||||
|
||||
data, err := network.ReadEndpoint(mentixURL, nil, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to read the Mentix endpoint")
|
||||
}
|
||||
|
||||
// Decode the data into a simplified, reduced data type
|
||||
type siteData struct {
|
||||
Sites []SiteInformation
|
||||
}
|
||||
sites := siteData{}
|
||||
if err := json.Unmarshal(data, &sites); err != nil {
|
||||
return nil, errors.Wrap(err, "error while decoding the JSON data")
|
||||
}
|
||||
|
||||
// Sort the sites alphabetically by their names
|
||||
sort.Slice(sites.Sites, func(i, j int) bool {
|
||||
return sites.Sites[i].Name < sites.Sites[j].Name
|
||||
})
|
||||
|
||||
return sites.Sites, nil
|
||||
}
|
||||
|
||||
// QuerySiteName uses Mentix to query the name of a site given by its ID.
|
||||
func QuerySiteName(siteID string, fullName bool, mentixHost, dataEndpoint string) (string, error) {
|
||||
sites, err := QueryAvailableSites(mentixHost, dataEndpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
index := len(sites)
|
||||
for i, site := range sites {
|
||||
if site.ID == siteID {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index != len(sites) {
|
||||
if fullName {
|
||||
return sites[index].FullName, nil
|
||||
}
|
||||
|
||||
return sites[index].Name, nil
|
||||
}
|
||||
|
||||
return "", errors.Errorf("no site with ID %v found", siteID)
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this filePath 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 data
|
||||
|
||||
// Storage defines the interface for sites and accounts storages.
|
||||
type Storage interface {
|
||||
// ReadSites reads all stored sites into the given data object.
|
||||
ReadSites() (*Sites, error)
|
||||
// WriteSites writes all stored sites from the given data object.
|
||||
WriteSites(sites *Sites) error
|
||||
|
||||
// SiteAdded is called when a site has been added.
|
||||
SiteAdded(site *Site)
|
||||
// SiteUpdated is called when a site has been updated.
|
||||
SiteUpdated(site *Site)
|
||||
// SiteRemoved is called when a site has been removed.
|
||||
SiteRemoved(site *Site)
|
||||
|
||||
// ReadAccounts reads all stored accounts into the given data object.
|
||||
ReadAccounts() (*Accounts, error)
|
||||
// WriteAccounts writes all stored accounts from the given data object.
|
||||
WriteAccounts(accounts *Accounts) error
|
||||
|
||||
// AccountAdded is called when an account has been added.
|
||||
AccountAdded(account *Account)
|
||||
// AccountUpdated is called when an account has been updated.
|
||||
AccountUpdated(account *Account)
|
||||
// AccountRemoved is called when an account has been removed.
|
||||
AccountRemoved(account *Account)
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// 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 email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/smtpclient"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type emailData struct {
|
||||
Account *data.Account
|
||||
|
||||
AccountsAddress string
|
||||
GOCDBAddress string
|
||||
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
// SendFunction is the definition of email send functions.
|
||||
type SendFunction = func(*data.Account, []string, map[string]string, config.Configuration) error
|
||||
|
||||
func getEmailData(account *data.Account, conf config.Configuration, params map[string]string) *emailData {
|
||||
return &emailData{
|
||||
Account: account,
|
||||
AccountsAddress: conf.Webserver.URL,
|
||||
GOCDBAddress: conf.GOCDB.URL,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
// SendAccountCreated sends an email about account creation.
|
||||
func SendAccountCreated(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
return send(recipients, "ScienceMesh: Site Administrator Account created", accountCreatedTemplate, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
// SendSiteAccessGranted sends an email about granted Site access.
|
||||
func SendSiteAccessGranted(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
return send(recipients, "ScienceMesh: Site access granted", siteAccessGrantedTemplate, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
// SendGOCDBAccessGranted sends an email about granted GOCDB access.
|
||||
func SendGOCDBAccessGranted(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
return send(recipients, "ScienceMesh: GOCDB access granted", gocdbAccessGrantedTemplate, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
// SendPasswordReset sends an email containing the user's new password.
|
||||
func SendPasswordReset(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
return send(recipients, "ScienceMesh: Password reset", passwordResetTemplate, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
// SendContactForm sends a generic contact form to the ScienceMesh admins.
|
||||
func SendContactForm(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
return send(recipients, "ScienceMesh: Contact form", contactFormTemplate, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
// SendAlertNotification sends an alert via email.
|
||||
func SendAlertNotification(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
subject := params["Summary"]
|
||||
tpl := alertFiringNotificationTemplate
|
||||
if strings.EqualFold(params["Status"], "resolved") {
|
||||
tpl = alertResolvedNotificationTemplate
|
||||
subject += " [RESOLVED]"
|
||||
}
|
||||
return send(recipients, "ScienceMesh Alert: "+subject, tpl, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
func send(recipients []string, subject string, bodyTemplate string, data interface{}, smtp *smtpclient.SMTPCredentials) error {
|
||||
// Do not fail if no SMTP client or recipient is given
|
||||
if smtp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tpl := template.New("email")
|
||||
prepareEmailTemplate(tpl)
|
||||
|
||||
if _, err := tpl.Parse(bodyTemplate); err != nil {
|
||||
return errors.Wrap(err, "error while parsing email template")
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
if err := tpl.Execute(&body, data); err != nil {
|
||||
return errors.Wrap(err, "error while executing email template")
|
||||
}
|
||||
|
||||
for _, recipient := range recipients {
|
||||
if len(recipient) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Send the mail w/o blocking the main thread
|
||||
go func(recipient string) {
|
||||
_ = smtp.SendMail(recipient, subject, body.String())
|
||||
}(recipient)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareEmailTemplate(tpl *template.Template) {
|
||||
// Add some custom helper functions to the template
|
||||
tpl.Funcs(template.FuncMap{
|
||||
"indent": func(n int, s string) string {
|
||||
lines := make([]string, 0, 10)
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
line = strings.Repeat(" ", n) + line
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
},
|
||||
})
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// 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 email
|
||||
|
||||
const accountCreatedTemplate = `
|
||||
Dear {{.Account.FirstName}} {{.Account.LastName}},
|
||||
|
||||
Your ScienceMesh Site Administrator Account has been successfully created!
|
||||
|
||||
Log in to your account by visiting the user account panel:
|
||||
{{.AccountsAddress}}
|
||||
|
||||
Using this panel, you can modify your information, request access to the GOCDB, and more.
|
||||
|
||||
Kind regards,
|
||||
The ScienceMesh Team
|
||||
`
|
||||
|
||||
const siteAccessGrantedTemplate = `
|
||||
Dear {{.Account.FirstName}} {{.Account.LastName}},
|
||||
|
||||
You have been granted access to the global configuration of your site.
|
||||
|
||||
Log in to your account to access this configuration:
|
||||
{{.AccountsAddress}}
|
||||
|
||||
Kind regards,
|
||||
The ScienceMesh Team
|
||||
`
|
||||
|
||||
const gocdbAccessGrantedTemplate = `
|
||||
Dear {{.Account.FirstName}} {{.Account.LastName}},
|
||||
|
||||
You have been granted access to the ScienceMesh GOCDB instance:
|
||||
{{.GOCDBAddress}}
|
||||
|
||||
Simply use your regular ScienceMesh Site Administrator Account credentials to log in to the GOCDB.
|
||||
|
||||
Kind regards,
|
||||
The ScienceMesh Team
|
||||
`
|
||||
|
||||
const passwordResetTemplate = `
|
||||
Dear {{.Account.FirstName}} {{.Account.LastName}},
|
||||
|
||||
Your password has been successfully reset!
|
||||
Your new password is: {{.Account.Password.Value}}
|
||||
|
||||
We recommend to change this password immediately after logging in.
|
||||
|
||||
Kind regards,
|
||||
The ScienceMesh Team
|
||||
`
|
||||
|
||||
const contactFormTemplate = `
|
||||
{{.Account.FirstName}} {{.Account.LastName}} ({{.Account.Email}}) has sent the following message:
|
||||
|
||||
{{.Params.Subject}}
|
||||
---------------------------------------------------------------------------------------------------
|
||||
|
||||
{{.Params.Message}}
|
||||
`
|
||||
|
||||
const alertFiringNotificationTemplate = `
|
||||
Site '{{.Params.Site}}' has generated an alert:
|
||||
|
||||
Type: {{.Params.Name}}
|
||||
Service: {{.Params.Service}}
|
||||
Instance: {{.Params.Instance}}
|
||||
Job: {{.Params.Job}}
|
||||
Severity: {{.Params.Severity}}
|
||||
|
||||
{{.Params.Description | indent 2}}
|
||||
|
||||
{{.Params.StartDate}} ({{.Params.Fingerprint}})
|
||||
`
|
||||
|
||||
const alertResolvedNotificationTemplate = `
|
||||
Site '{{.Params.Site}}' has resolved an alert:
|
||||
|
||||
Type: {{.Params.Name}}
|
||||
Service: {{.Params.Service}}
|
||||
Instance: {{.Params.Instance}}
|
||||
Job: {{.Params.Job}}
|
||||
Severity: {{.Params.Severity}}
|
||||
|
||||
{{.Params.Description | indent 2}}
|
||||
|
||||
{{.Params.StartDate}} - {{.Params.EndDate}} ({{.Params.Fingerprint}})
|
||||
`
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
// 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 siteacc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/manager"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
)
|
||||
|
||||
const (
|
||||
invokerUser = "user"
|
||||
)
|
||||
|
||||
type methodCallback = func(*SiteAccounts, url.Values, []byte, *html.Session) (interface{}, error)
|
||||
type accessSetterCallback = func(*manager.AccountsManager, *data.Account, bool) error
|
||||
|
||||
type endpoint struct {
|
||||
Path string
|
||||
Handler func(*SiteAccounts, endpoint, http.ResponseWriter, *http.Request, *html.Session)
|
||||
MethodCallbacks map[string]methodCallback
|
||||
IsPublic bool
|
||||
}
|
||||
|
||||
func createMethodCallbacks(cbGet methodCallback, cbPost methodCallback) map[string]methodCallback {
|
||||
callbacks := make(map[string]methodCallback)
|
||||
|
||||
if cbGet != nil {
|
||||
callbacks[http.MethodGet] = cbGet
|
||||
}
|
||||
|
||||
if cbPost != nil {
|
||||
callbacks[http.MethodPost] = cbPost
|
||||
}
|
||||
|
||||
return callbacks
|
||||
}
|
||||
|
||||
func getEndpoints() []endpoint {
|
||||
endpoints := []endpoint{
|
||||
// Form/panel endpoints
|
||||
{config.EndpointAdministration, callAdministrationEndpoint, nil, false},
|
||||
{config.EndpointAccount, callAccountEndpoint, nil, true},
|
||||
// General account endpoints
|
||||
{config.EndpointList, callMethodEndpoint, createMethodCallbacks(handleList, nil), false},
|
||||
{config.EndpointFind, callMethodEndpoint, createMethodCallbacks(handleFind, nil), false},
|
||||
{config.EndpointCreate, callMethodEndpoint, createMethodCallbacks(nil, handleCreate), true},
|
||||
{config.EndpointUpdate, callMethodEndpoint, createMethodCallbacks(nil, handleUpdate), false},
|
||||
{config.EndpointConfigure, callMethodEndpoint, createMethodCallbacks(nil, handleConfigure), false},
|
||||
{config.EndpointRemove, callMethodEndpoint, createMethodCallbacks(nil, handleRemove), false},
|
||||
// Site endpoints
|
||||
{config.EndpointSiteGet, callMethodEndpoint, createMethodCallbacks(handleSiteGet, nil), false},
|
||||
{config.EndpointSiteConfigure, callMethodEndpoint, createMethodCallbacks(nil, handleSiteConfigure), false},
|
||||
// Login endpoints
|
||||
{config.EndpointLogin, callMethodEndpoint, createMethodCallbacks(nil, handleLogin), true},
|
||||
{config.EndpointLogout, callMethodEndpoint, createMethodCallbacks(handleLogout, nil), true},
|
||||
{config.EndpointResetPassword, callMethodEndpoint, createMethodCallbacks(nil, handleResetPassword), true},
|
||||
{config.EndpointContact, callMethodEndpoint, createMethodCallbacks(nil, handleContact), true},
|
||||
// Authentication endpoints
|
||||
{config.EndpointVerifyUserToken, callMethodEndpoint, createMethodCallbacks(handleVerifyUserToken, nil), true},
|
||||
// Access management endpoints
|
||||
{config.EndpointGrantSiteAccess, callMethodEndpoint, createMethodCallbacks(nil, handleGrantSiteAccess), false},
|
||||
{config.EndpointGrantGOCDBAccess, callMethodEndpoint, createMethodCallbacks(nil, handleGrantGOCDBAccess), false},
|
||||
// Alerting endpoints
|
||||
{config.EndpointDispatchAlert, callMethodEndpoint, createMethodCallbacks(nil, handleDispatchAlert), false},
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func callAdministrationEndpoint(siteacc *SiteAccounts, ep endpoint, w http.ResponseWriter, r *http.Request, session *html.Session) {
|
||||
if err := siteacc.ShowAdministrationPanel(w, r, session); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprintf(w, "Unable to show the administration panel: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func callAccountEndpoint(siteacc *SiteAccounts, ep endpoint, w http.ResponseWriter, r *http.Request, session *html.Session) {
|
||||
if err := siteacc.ShowAccountPanel(w, r, session); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprintf(w, "Unable to show the account panel: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func callMethodEndpoint(siteacc *SiteAccounts, ep endpoint, w http.ResponseWriter, r *http.Request, session *html.Session) {
|
||||
// Every request to the accounts service results in a standardized JSON response
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// The default response is an unknown requestHandler (for the specified method)
|
||||
resp := Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("unknown endpoint %v for method %v", r.URL.Path, r.Method),
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
if ep.MethodCallbacks != nil {
|
||||
// Search for a matching method in the list of callbacks
|
||||
for method, cb := range ep.MethodCallbacks {
|
||||
if method == r.Method {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
|
||||
if respData, err := cb(siteacc, r.URL.Query(), body, session); err == nil {
|
||||
resp.Success = true
|
||||
resp.Error = ""
|
||||
resp.Data = respData
|
||||
} else {
|
||||
resp.Success = false
|
||||
resp.Error = fmt.Sprintf("%v", err)
|
||||
resp.Data = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Any failure during query handling results in a bad request
|
||||
if !resp.Success {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Responses here are always JSON
|
||||
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
||||
|
||||
jsonData, _ := json.MarshalIndent(&resp, "", "\t")
|
||||
_, _ = w.Write(jsonData)
|
||||
}
|
||||
|
||||
func handleList(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
return siteacc.AccountsManager().CloneAccounts(true), nil
|
||||
}
|
||||
|
||||
func handleFind(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := findAccount(siteacc, values.Get("by"), values.Get("value"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"account": account.Clone(true)}, nil
|
||||
}
|
||||
|
||||
func handleCreate(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleUpdate(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
email, setPassword, err := processInvoker(siteacc, values, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account.Email = email
|
||||
|
||||
// Update the account through the accounts manager
|
||||
if err := siteacc.AccountsManager().UpdateAccount(account, setPassword, false); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to update account")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleConfigure(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
email, _, err := processInvoker(siteacc, values, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account.Email = email
|
||||
|
||||
// Configure the account through the accounts manager
|
||||
if err := siteacc.AccountsManager().ConfigureAccount(account); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to configure account")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleRemove(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remove the account through the accounts manager
|
||||
if err := siteacc.AccountsManager().RemoveAccount(account); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to remove account")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleSiteGet(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
siteID := values.Get("site")
|
||||
if siteID == "" {
|
||||
return nil, errors.Errorf("no site specified")
|
||||
}
|
||||
site := siteacc.SitesManager().FindSite(siteID)
|
||||
if site == nil {
|
||||
return nil, errors.Errorf("no site with ID %v exists", siteID)
|
||||
}
|
||||
return map[string]interface{}{"site": site.Clone(false)}, nil
|
||||
}
|
||||
|
||||
func handleSiteConfigure(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
email, _, err := processInvoker(siteacc, values, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account, err := siteacc.AccountsManager().FindAccount(manager.FindByEmail, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
siteData := &data.Site{}
|
||||
if err := json.Unmarshal(body, siteData); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid form data")
|
||||
}
|
||||
siteData.ID = account.Site
|
||||
|
||||
// Configure the site through the sites manager
|
||||
if err := siteacc.SitesManager().UpdateSite(siteData); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to configure site")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleLogin(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Login the user through the users manager
|
||||
token, err := siteacc.UsersManager().LoginUser(account.Email, account.Password.Value, values.Get("scope"), session)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to login user")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func handleLogout(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
// Logout the user through the users manager
|
||||
siteacc.UsersManager().LogoutUser(session)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleResetPassword(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reset the password through the users manager
|
||||
if err := siteacc.AccountsManager().ResetPassword(account.Email); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to reset password")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleContact(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
if !session.IsUserLoggedIn() {
|
||||
return nil, errors.Errorf("no user is currently logged in")
|
||||
}
|
||||
|
||||
type jsonData struct {
|
||||
Subject string `json:"subject"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
contactData := &jsonData{}
|
||||
if err := json.Unmarshal(body, contactData); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid form data")
|
||||
}
|
||||
|
||||
// Send an email through the accounts manager
|
||||
siteacc.AccountsManager().SendContactForm(session.LoggedInUser().Account, strings.TrimSpace(contactData.Subject), strings.TrimSpace(contactData.Message))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleVerifyUserToken(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
token := values.Get("token")
|
||||
if token == "" {
|
||||
return nil, errors.Errorf("no token specified")
|
||||
}
|
||||
|
||||
user := values.Get("user")
|
||||
if user == "" {
|
||||
return nil, errors.Errorf("no user specified")
|
||||
}
|
||||
|
||||
// Verify the user token using the users manager
|
||||
newToken, err := siteacc.UsersManager().VerifyUserToken(token, user, values.Get("scope"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "token verification failed")
|
||||
}
|
||||
|
||||
return newToken, nil
|
||||
}
|
||||
|
||||
func handleDispatchAlert(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
alertsData := &template.Data{}
|
||||
if err := json.Unmarshal(body, alertsData); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to unmarshal the alerts data")
|
||||
}
|
||||
|
||||
// Dispatch the alerts using the alerts dispatcher
|
||||
if err := siteacc.AlertsDispatcher().DispatchAlerts(alertsData, siteacc.AccountsManager().CloneAccounts(true)); err != nil {
|
||||
return nil, errors.Wrap(err, "error while dispatching the alerts")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleGrantSiteAccess(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
return handleGrantAccess((*manager.AccountsManager).GrantSiteAccess, siteacc, values, body, session)
|
||||
}
|
||||
|
||||
func handleGrantGOCDBAccess(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
return handleGrantAccess((*manager.AccountsManager).GrantGOCDBAccess, siteacc, values, body, session)
|
||||
}
|
||||
|
||||
func handleGrantAccess(accessSetter accessSetterCallback, siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if val := values.Get("status"); len(val) > 0 {
|
||||
var grantAccess bool
|
||||
switch strings.ToLower(val) {
|
||||
case "true":
|
||||
grantAccess = true
|
||||
|
||||
case "false":
|
||||
grantAccess = false
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported access status %v", val[0])
|
||||
}
|
||||
|
||||
// Grant access to the account through the accounts manager
|
||||
if err := accessSetter(siteacc.AccountsManager(), account, grantAccess); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to change the access status of the account")
|
||||
}
|
||||
} else {
|
||||
return nil, errors.Errorf("no access status provided")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func unmarshalRequestData(body []byte) (*data.Account, error) {
|
||||
account := &data.Account{}
|
||||
if err := json.Unmarshal(body, account); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid account data")
|
||||
}
|
||||
account.Cleanup()
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func findAccount(siteacc *SiteAccounts, by string, value string) (*data.Account, error) {
|
||||
if len(by) == 0 && len(value) == 0 {
|
||||
return nil, errors.Errorf("missing search criteria")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func processInvoker(siteacc *SiteAccounts, values url.Values, session *html.Session) (string, bool, error) {
|
||||
var email string
|
||||
var invokedByUser bool
|
||||
|
||||
switch strings.ToLower(values.Get("invoker")) {
|
||||
case invokerUser:
|
||||
// If this endpoint was called by the user, set the account email from the stored session
|
||||
if !session.IsUserLoggedIn() {
|
||||
return "", false, errors.Errorf("no user is currently logged in")
|
||||
}
|
||||
|
||||
email = session.LoggedInUser().Account.Email
|
||||
invokedByUser = true
|
||||
|
||||
default:
|
||||
return "", false, errors.Errorf("no invoker provided")
|
||||
}
|
||||
|
||||
return email, invokedByUser, nil
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
// 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 html
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// TemplateID is the type for template identifiers.
|
||||
type TemplateID = string
|
||||
|
||||
// Panel provides basic HTML panel functionality.
|
||||
type Panel struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
name string
|
||||
|
||||
provider PanelProvider
|
||||
|
||||
templates map[TemplateID]*template.Template
|
||||
}
|
||||
|
||||
const (
|
||||
pathParameterName = "path"
|
||||
)
|
||||
|
||||
func (panel *Panel) initialize(name string, provider PanelProvider, conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if name == "" {
|
||||
return errors.Errorf("no name provided")
|
||||
}
|
||||
panel.name = name
|
||||
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
panel.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
panel.log = log
|
||||
|
||||
if provider == nil {
|
||||
return errors.Errorf("no panel provider provided")
|
||||
}
|
||||
panel.provider = provider
|
||||
|
||||
// Create space for the panel templates
|
||||
panel.templates = make(map[string]*template.Template, 5)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (panel *Panel) compile(provider ContentProvider) (string, error) {
|
||||
content := panelTemplate
|
||||
|
||||
// Replace placeholders by the values provided by the content provider
|
||||
content = strings.ReplaceAll(content, "$(TITLE)", provider.GetTitle())
|
||||
content = strings.ReplaceAll(content, "$(CAPTION)", provider.GetCaption())
|
||||
|
||||
content = strings.ReplaceAll(content, "$(CONTENT_JAVASCRIPT)", provider.GetContentJavaScript())
|
||||
content = strings.ReplaceAll(content, "$(CONTENT_STYLESHEET)", provider.GetContentStyleSheet())
|
||||
content = strings.ReplaceAll(content, "$(CONTENT_BODY)", provider.GetContentBody())
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// AddTemplate adds and compiles a new template.
|
||||
func (panel *Panel) AddTemplate(name TemplateID, provider ContentProvider) error {
|
||||
name = panel.getFullTemplateName(name)
|
||||
|
||||
if provider == nil {
|
||||
return errors.Errorf("no content provider provided")
|
||||
}
|
||||
|
||||
content, err := panel.compile(provider)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error while compiling panel template %v", name)
|
||||
}
|
||||
|
||||
tpl := template.New(name)
|
||||
panel.prepareTemplate(tpl)
|
||||
|
||||
if _, err := tpl.Parse(content); err != nil {
|
||||
return errors.Wrapf(err, "error while parsing panel template %v", name)
|
||||
}
|
||||
panel.templates[name] = tpl
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute generates the HTTP output of the panel and writes it to the response writer.
|
||||
func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *Session, dataProvider PanelDataProvider) error {
|
||||
// Get the path query parameter; the panel provider may use this to determine the template to use
|
||||
path := r.URL.Query().Get(pathParameterName)
|
||||
|
||||
actTpl := panel.provider.GetActiveTemplate(session, path)
|
||||
tplName := panel.getFullTemplateName(actTpl)
|
||||
tpl, ok := panel.templates[tplName]
|
||||
if !ok {
|
||||
return errors.Errorf("template %v not found", tplName)
|
||||
}
|
||||
|
||||
// If a data provider is specified, use it to get additional template data
|
||||
var data interface{}
|
||||
if dataProvider != nil {
|
||||
data = dataProvider(session)
|
||||
}
|
||||
|
||||
// Perform the pre-execution phase in which the panel provider can intercept the actual execution
|
||||
if state, err := panel.provider.PreExecute(session, actTpl, w, r); err == nil {
|
||||
if !state {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return errors.Wrapf(err, "pre-execution of template %v failed", tplName)
|
||||
}
|
||||
|
||||
return tpl.Execute(w, data)
|
||||
}
|
||||
|
||||
func (panel *Panel) prepareTemplate(tpl *template.Template) {
|
||||
// Add some custom helper functions to the template
|
||||
tpl.Funcs(template.FuncMap{
|
||||
"getServerAddress": func() string {
|
||||
return strings.TrimRight(panel.conf.Webserver.URL, "/")
|
||||
},
|
||||
"getSiteName": func(siteID string, fullName bool) string {
|
||||
siteName, _ := data.QuerySiteName(siteID, fullName, panel.conf.Mentix.URL, panel.conf.Mentix.DataEndpoint)
|
||||
return siteName
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (panel *Panel) getFullTemplateName(name string) string {
|
||||
return panel.name + "-" + name
|
||||
}
|
||||
|
||||
// NewPanel creates a new panel.
|
||||
func NewPanel(name string, provider PanelProvider, conf *config.Configuration, log *zerolog.Logger) (*Panel, error) {
|
||||
panel := &Panel{}
|
||||
if err := panel.initialize(name, provider, conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the panel")
|
||||
}
|
||||
return panel, nil
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// 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 html
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
// ContinueExecution causes the execution of a panel to continue.
|
||||
ContinueExecution = true
|
||||
// AbortExecution causes the execution of a panel to be aborted.
|
||||
AbortExecution = false
|
||||
)
|
||||
|
||||
// ExecutionResult is the type returned by the PreExecute function of PanelProvider.
|
||||
type ExecutionResult = bool
|
||||
|
||||
// PanelProvider handles general panel tasks.
|
||||
type PanelProvider interface {
|
||||
// GetActiveTemplate returns the name of the active template.
|
||||
GetActiveTemplate(*Session, string) string
|
||||
|
||||
// PreExecute is called before the actual template is being executed.
|
||||
PreExecute(*Session, string, http.ResponseWriter, *http.Request) (ExecutionResult, error)
|
||||
}
|
||||
|
||||
// PanelDataProvider is the function signature for panel data providers.
|
||||
type PanelDataProvider = func(*Session) interface{}
|
||||
|
||||
// ContentProvider defines various methods for HTML content providers.
|
||||
type ContentProvider interface {
|
||||
// GetTitle returns the title of the panel.
|
||||
GetTitle() string
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
GetCaption() string
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
GetContentJavaScript() string
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
GetContentStyleSheet() string
|
||||
// GetContentBody delivers the actual body content.
|
||||
GetContentBody() string
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
// 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 html
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Session stores all data associated with an HTML session.
|
||||
type Session struct {
|
||||
ID string
|
||||
MigrationID string
|
||||
RemoteAddress string
|
||||
CreationTime time.Time
|
||||
Timeout time.Duration
|
||||
|
||||
Data map[string]interface{}
|
||||
|
||||
loggedInUser *SessionUser
|
||||
|
||||
expirationTime time.Time
|
||||
halflifeTime time.Time
|
||||
|
||||
sessionCookieName string
|
||||
}
|
||||
|
||||
// SessionUser holds information about the logged in user
|
||||
type SessionUser struct {
|
||||
Account *data.Account
|
||||
Site *data.Site
|
||||
}
|
||||
|
||||
func getRemoteAddress(r *http.Request) string {
|
||||
// Remove the port number from the remote address
|
||||
remoteAddress := ""
|
||||
if address := strings.Split(r.RemoteAddr, ":"); len(address) == 2 {
|
||||
remoteAddress = address[0]
|
||||
}
|
||||
return remoteAddress
|
||||
}
|
||||
|
||||
// LoggedInUser retrieves the currently logged in user or nil if none is logged in.
|
||||
func (sess *Session) LoggedInUser() *SessionUser {
|
||||
return sess.loggedInUser
|
||||
}
|
||||
|
||||
// LoginUser logs in the provided user.
|
||||
func (sess *Session) LoginUser(acc *data.Account, site *data.Site) {
|
||||
sess.loggedInUser = &SessionUser{
|
||||
Account: acc,
|
||||
Site: site,
|
||||
}
|
||||
}
|
||||
|
||||
// LogoutUser logs out the currently logged in user.
|
||||
func (sess *Session) LogoutUser() {
|
||||
sess.loggedInUser = nil
|
||||
}
|
||||
|
||||
// IsUserLoggedIn tells whether a user is currently logged in.
|
||||
func (sess *Session) IsUserLoggedIn() bool {
|
||||
return sess.loggedInUser != nil
|
||||
}
|
||||
|
||||
// Save stores the session ID in a cookie using a response writer.
|
||||
func (sess *Session) Save(cookiePath string, w http.ResponseWriter) {
|
||||
fullURL, _ := url.Parse(cookiePath)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sess.sessionCookieName,
|
||||
Secure: !strings.EqualFold(fullURL.Hostname(), "localhost"),
|
||||
Value: sess.ID,
|
||||
MaxAge: int(sess.Timeout / time.Second),
|
||||
Domain: fullURL.Hostname(),
|
||||
Path: fullURL.Path,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyRequest checks whether the provided request matches the stored session.
|
||||
func (sess *Session) VerifyRequest(r *http.Request, verifyRemoteAddress bool) error {
|
||||
cookie, err := r.Cookie(sess.sessionCookieName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to retrieve client session ID")
|
||||
}
|
||||
if cookie.Value != sess.ID {
|
||||
return errors.Errorf("the session ID doesn't match")
|
||||
}
|
||||
|
||||
if verifyRemoteAddress && sess.RemoteAddress != "" {
|
||||
if !strings.EqualFold(getRemoteAddress(r), sess.RemoteAddress) {
|
||||
return errors.Errorf("remote address has changed (%v != %v)", r.RemoteAddr, sess.RemoteAddress)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HalftimePassed checks whether the session has passed the first half of its lifetime.
|
||||
func (sess *Session) HalftimePassed() bool {
|
||||
return time.Now().After(sess.halflifeTime)
|
||||
}
|
||||
|
||||
// HasExpired checks whether the session has reached is timeout.
|
||||
func (sess *Session) HasExpired() bool {
|
||||
return time.Now().After(sess.expirationTime)
|
||||
}
|
||||
|
||||
// NewSession creates a new session, giving it a random ID.
|
||||
func NewSession(name string, timeout time.Duration, r *http.Request) *Session {
|
||||
session := &Session{
|
||||
ID: uuid.NewString(),
|
||||
MigrationID: "",
|
||||
RemoteAddress: getRemoteAddress(r),
|
||||
CreationTime: time.Now(),
|
||||
Timeout: timeout,
|
||||
Data: make(map[string]interface{}, 10),
|
||||
loggedInUser: nil,
|
||||
expirationTime: time.Now().Add(timeout),
|
||||
halflifeTime: time.Now().Add(timeout / 2),
|
||||
sessionCookieName: name,
|
||||
}
|
||||
return session
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// 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 html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// SessionManager manages HTML sessions.
|
||||
type SessionManager struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
sessions map[string]*Session
|
||||
|
||||
sessionName string
|
||||
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (mngr *SessionManager) initialize(name string, conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if name == "" {
|
||||
return errors.Errorf("no session name provided")
|
||||
}
|
||||
mngr.sessionName = name
|
||||
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
mngr.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
mngr.log = log
|
||||
|
||||
mngr.sessions = make(map[string]*Session, 100)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleRequest performs all session-related tasks during an HTML request. Always returns a valid session object.
|
||||
func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request) (*Session, error) {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
var session *Session
|
||||
var sessionErr error
|
||||
|
||||
// Try to get the session ID from the request; if none has been set yet, a new one will be assigned
|
||||
cookie, err := r.Cookie(mngr.sessionName)
|
||||
if err == nil {
|
||||
session = mngr.findSession(cookie.Value)
|
||||
if session != nil {
|
||||
mngr.logSessionInfo(session, r, "existing session found")
|
||||
|
||||
// Verify the request against the session: If it is invalid, set an error; if the session has expired, create a new one; if it has already passed its halftime, migrate to a new one
|
||||
if err := session.VerifyRequest(r, mngr.conf.Webserver.VerifyRemoteAddress); err == nil {
|
||||
if session.HasExpired() {
|
||||
// The session has expired, so a new one needs to be created
|
||||
session = nil
|
||||
|
||||
mngr.logSessionInfo(session, r, "session expired")
|
||||
} else if session.HalftimePassed() {
|
||||
// The session has passed its halftime, so migrate it to a new one (makes hijacking session IDs harder)
|
||||
session, err = mngr.migrateSession(session, r)
|
||||
if err != nil {
|
||||
session = nil
|
||||
sessionErr = errors.Wrap(err, "unable to migrate session")
|
||||
}
|
||||
|
||||
mngr.logSessionInfo(session, r, "session migrated")
|
||||
}
|
||||
} else {
|
||||
session = nil
|
||||
sessionErr = errors.Wrap(err, "invalid session")
|
||||
|
||||
mngr.logSessionInfo(session, r, "session invalid (verify failed)")
|
||||
}
|
||||
}
|
||||
} else if err != http.ErrNoCookie {
|
||||
// The session cookie exists but seems to be invalid, so set an error
|
||||
session = nil
|
||||
sessionErr = errors.Wrap(err, "unable to get the session ID from the client")
|
||||
|
||||
mngr.logSessionInfo(session, r, fmt.Sprintf("session cookie error: %v", err))
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
// No session found for the client, so create a new one; this will always succeed
|
||||
session = mngr.createSession(r)
|
||||
|
||||
mngr.logSessionInfo(session, r, "assigned new session")
|
||||
}
|
||||
|
||||
// Store the session ID on the client side
|
||||
session.Save(mngr.conf.Webserver.URL, w)
|
||||
|
||||
return session, sessionErr
|
||||
}
|
||||
|
||||
// PurgeSessions removes any expired sessions.
|
||||
func (mngr *SessionManager) PurgeSessions() {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
var expiredSessions []string
|
||||
for id, session := range mngr.sessions {
|
||||
if session.HasExpired() {
|
||||
expiredSessions = append(expiredSessions, id)
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range expiredSessions {
|
||||
delete(mngr.sessions, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (mngr *SessionManager) createSession(r *http.Request) *Session {
|
||||
session := NewSession(mngr.sessionName, time.Duration(mngr.conf.Webserver.SessionTimeout)*time.Second, r)
|
||||
mngr.sessions[session.ID] = session
|
||||
return session
|
||||
}
|
||||
|
||||
func (mngr *SessionManager) findSession(id string) *Session {
|
||||
if session, ok := mngr.sessions[id]; ok {
|
||||
return session
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mngr *SessionManager) migrateSession(session *Session, r *http.Request) (*Session, error) {
|
||||
sessionNew := mngr.createSession(r)
|
||||
|
||||
// Carry over the old session information, thus preserving the existing session
|
||||
sessionNew.MigrationID = session.ID
|
||||
sessionNew.Data = session.Data
|
||||
|
||||
if user := session.LoggedInUser(); user != nil {
|
||||
sessionNew.LoginUser(user.Account, user.Site)
|
||||
} else {
|
||||
sessionNew.LogoutUser()
|
||||
}
|
||||
|
||||
// Delete the old session
|
||||
delete(mngr.sessions, session.ID)
|
||||
|
||||
return sessionNew, nil
|
||||
}
|
||||
|
||||
func (mngr *SessionManager) logSessionInfo(session *Session, r *http.Request, info string) {
|
||||
if mngr.conf.Webserver.LogSessions {
|
||||
if session != nil {
|
||||
mngr.log.Debug().Str("id", session.ID).Str("address", r.RemoteAddr).Str("path", r.URL.Path).Msg(info)
|
||||
} else {
|
||||
mngr.log.Debug().Str("address", r.RemoteAddr).Str("path", r.URL.Path).Msg(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewSessionManager creates a new session manager.
|
||||
func NewSessionManager(name string, conf *config.Configuration, log *zerolog.Logger) (*SessionManager, error) {
|
||||
mngr := &SessionManager{}
|
||||
if err := mngr.initialize(name, conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the session manager")
|
||||
}
|
||||
return mngr, nil
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
// 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 html
|
||||
|
||||
const panelTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
const STATE_NONE = 0
|
||||
const STATE_STATUS = 1
|
||||
const STATE_SUCCESS = 2
|
||||
const STATE_ERROR = 3
|
||||
|
||||
function enableForm(id, enable) {
|
||||
var form = document.getElementById(id);
|
||||
var elements = form.elements;
|
||||
for (var i = 0, len = elements.length; i < len; ++i) {
|
||||
elements[i].disabled = !enable;
|
||||
}
|
||||
}
|
||||
|
||||
function setElementVisibility(id, visible) {
|
||||
var elem = document.getElementById(id);
|
||||
if (visible) {
|
||||
elem.classList.add("visible");
|
||||
elem.classList.remove("hidden");
|
||||
} else {
|
||||
elem.classList.remove("visible");
|
||||
elem.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function setState(state, msg = "", formId = null, focusElem = null, formState = null) {
|
||||
setElementVisibility("status", state == STATE_STATUS);
|
||||
setElementVisibility("success", state == STATE_SUCCESS);
|
||||
setElementVisibility("error", state == STATE_ERROR);
|
||||
|
||||
var elem = null;
|
||||
switch (state) {
|
||||
case STATE_STATUS:
|
||||
elem = document.getElementById("status");
|
||||
break;
|
||||
|
||||
case STATE_SUCCESS:
|
||||
elem = document.getElementById("success");
|
||||
break;
|
||||
|
||||
case STATE_ERROR:
|
||||
elem = document.getElementById("error");
|
||||
break;
|
||||
}
|
||||
|
||||
if (elem !== null) {
|
||||
elem.innerHTML = msg;
|
||||
}
|
||||
|
||||
if (formId !== null && formState !== null) {
|
||||
enableForm(formId, formState);
|
||||
}
|
||||
|
||||
if (focusElem !== null) {
|
||||
var elem = document.getElementById(focusElem);
|
||||
elem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
FormData.prototype.getTrimmed = function(id) {
|
||||
var val = this.get(id);
|
||||
|
||||
if (val != null) {
|
||||
return val.trim();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
$(CONTENT_JAVASCRIPT)
|
||||
</script>
|
||||
<style>
|
||||
form {
|
||||
border-color: lightgray !important;
|
||||
}
|
||||
button {
|
||||
min-width: 140px;
|
||||
}
|
||||
input {
|
||||
width: 95%;
|
||||
}
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 100%;
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
.container {
|
||||
width: 900px;
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
.container-inline {
|
||||
display: inline-grid;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
.status {
|
||||
border-color: #F7B22A;
|
||||
background: #FFEABF;
|
||||
}
|
||||
.success {
|
||||
border-color: #3CAC3A;
|
||||
background: #D3EFD2;
|
||||
}
|
||||
.error {
|
||||
border-color: #F20000;
|
||||
background: #F4D0D0;
|
||||
}
|
||||
.visible {
|
||||
display: block;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
$(CONTENT_STYLESHEET)
|
||||
</style>
|
||||
<title>$(TITLE)</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div><h1>$(CAPTION)</h1></div>
|
||||
|
||||
$(CONTENT_BODY)
|
||||
|
||||
<div id="status" class="box status hidden">
|
||||
</div>
|
||||
<div id="success" class="box success hidden">
|
||||
</div>
|
||||
<div id="error" class="box error hidden">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// 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 manager
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
|
||||
// AccountsListenerCallback is the generic function type for accounts listeners.
|
||||
type AccountsListenerCallback = func(AccountsListener, *data.Account)
|
||||
|
||||
// AccountsListener is an interface that listens to accounts events.
|
||||
type AccountsListener interface {
|
||||
// AccountCreated is called whenever an account was created.
|
||||
AccountCreated(account *data.Account)
|
||||
// AccountUpdated is called whenever an account was updated.
|
||||
AccountUpdated(account *data.Account)
|
||||
// AccountRemoved is called whenever an account was removed.
|
||||
AccountRemoved(account *data.Account)
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
// 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 manager
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/email"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/manager/gocdb"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/smtpclient"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
)
|
||||
|
||||
const (
|
||||
// FindByEmail holds the string value of the corresponding search criterium.
|
||||
FindByEmail = "email"
|
||||
)
|
||||
|
||||
// AccountsManager is responsible for all site account related tasks.
|
||||
type AccountsManager struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
storage data.Storage
|
||||
|
||||
accounts data.Accounts
|
||||
accountsListeners []AccountsListener
|
||||
|
||||
smtp *smtpclient.SMTPCredentials
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) initialize(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
mngr.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
mngr.log = log
|
||||
|
||||
if storage == nil {
|
||||
return errors.Errorf("no storage provided")
|
||||
}
|
||||
mngr.storage = storage
|
||||
|
||||
mngr.accounts = make(data.Accounts, 0, 32) // Reserve some space for accounts
|
||||
mngr.readAllAccounts()
|
||||
|
||||
// Register accounts listeners
|
||||
if listener, err := gocdb.NewListener(mngr.conf, mngr.log); err == nil {
|
||||
mngr.accountsListeners = append(mngr.accountsListeners, listener)
|
||||
} else {
|
||||
return errors.Wrap(err, "unable to create the GOCDB accounts listener")
|
||||
}
|
||||
|
||||
// Create the SMTP client
|
||||
if conf.Email.SMTP != nil {
|
||||
mngr.smtp = smtpclient.NewSMTPCredentials(conf.Email.SMTP)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) readAllAccounts() {
|
||||
if accounts, err := mngr.storage.ReadAccounts(); err == nil {
|
||||
mngr.accounts = *accounts
|
||||
} else {
|
||||
// Just warn when not being able to read accounts
|
||||
mngr.log.Warn().Err(err).Msg("error while reading accounts")
|
||||
}
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) writeAllAccounts() {
|
||||
if err := mngr.storage.WriteAccounts(&mngr.accounts); err != nil {
|
||||
// Just warn when not being able to write accounts
|
||||
mngr.log.Warn().Err(err).Msg("error while writing accounts")
|
||||
}
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) findAccount(by string, value string) (*data.Account, error) {
|
||||
if len(value) == 0 {
|
||||
return nil, errors.Errorf("no search value specified")
|
||||
}
|
||||
|
||||
var account *data.Account
|
||||
switch strings.ToLower(by) {
|
||||
case FindByEmail:
|
||||
account = mngr.findAccountByPredicate(func(account *data.Account) bool { return strings.EqualFold(account.Email, value) })
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("invalid search type %v", by)
|
||||
}
|
||||
|
||||
if account != nil {
|
||||
return account, nil
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("no user found matching the specified criteria")
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) findAccountByPredicate(predicate func(*data.Account) bool) *data.Account {
|
||||
for _, account := range mngr.accounts {
|
||||
if predicate(account) {
|
||||
return account
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAccount creates a new account; if an account with the same email address already exists, an error is returned.
|
||||
func (mngr *AccountsManager) CreateAccount(accountData *data.Account) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
// Accounts must be unique (identified by their email address)
|
||||
if account, _ := mngr.findAccount(FindByEmail, accountData.Email); account != nil {
|
||||
return errors.Errorf("an account with the specified email address already exists")
|
||||
}
|
||||
|
||||
if account, err := data.NewAccount(accountData.Email, accountData.Title, accountData.FirstName, accountData.LastName, accountData.Site, accountData.Role, accountData.PhoneNumber, accountData.Password.Value); err == nil {
|
||||
mngr.accounts = append(mngr.accounts, account)
|
||||
mngr.storage.AccountAdded(account)
|
||||
mngr.writeAllAccounts()
|
||||
|
||||
mngr.sendEmail(account, nil, email.SendAccountCreated)
|
||||
mngr.callListeners(account, AccountsListener.AccountCreated)
|
||||
} else {
|
||||
return errors.Wrap(err, "error while creating account")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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, setPassword bool, copyData bool) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
account, err := mngr.findAccount(FindByEmail, accountData.Email)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "user to update not found")
|
||||
}
|
||||
|
||||
if err := account.Update(accountData, setPassword, copyData); err == nil {
|
||||
account.DateModified = time.Now()
|
||||
|
||||
mngr.storage.AccountUpdated(account)
|
||||
mngr.writeAllAccounts()
|
||||
|
||||
mngr.callListeners(account, AccountsListener.AccountUpdated)
|
||||
} else {
|
||||
return errors.Wrap(err, "error while updating account")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigureAccount configures the account identified by the account email; if no such account exists, an error is returned.
|
||||
func (mngr *AccountsManager) ConfigureAccount(accountData *data.Account) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
account, err := mngr.findAccount(FindByEmail, accountData.Email)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "user to configure not found")
|
||||
}
|
||||
|
||||
if err := account.Configure(accountData); err == nil {
|
||||
account.DateModified = time.Now()
|
||||
|
||||
mngr.storage.AccountUpdated(account)
|
||||
mngr.writeAllAccounts()
|
||||
|
||||
mngr.callListeners(account, AccountsListener.AccountUpdated)
|
||||
} else {
|
||||
return errors.Wrap(err, "error while configuring account")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword resets the password for the given user.
|
||||
func (mngr *AccountsManager) ResetPassword(name string) error {
|
||||
account, err := mngr.findAccount(FindByEmail, name)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "user to reset password for not found")
|
||||
}
|
||||
accountUpd := account.Clone(true)
|
||||
accountUpd.Password.Value = password.MustGenerate(defaultPasswordLength, 2, 0, false, true)
|
||||
|
||||
err = mngr.UpdateAccount(accountUpd, true, false)
|
||||
if err == nil {
|
||||
mngr.sendEmail(accountUpd, nil, email.SendPasswordReset)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FindAccount is used to find an account by various criteria. The account is cloned to prevent data changes.
|
||||
func (mngr *AccountsManager) FindAccount(by string, value string) (*data.Account, error) {
|
||||
return mngr.FindAccountEx(by, value, true)
|
||||
}
|
||||
|
||||
// FindAccountEx is used to find an account by various criteria and optionally clone the account.
|
||||
func (mngr *AccountsManager) FindAccountEx(by string, value string, cloneAccount bool) (*data.Account, error) {
|
||||
mngr.mutex.RLock()
|
||||
defer mngr.mutex.RUnlock()
|
||||
|
||||
account, err := mngr.findAccount(by, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cloneAccount {
|
||||
account = account.Clone(false)
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// GrantSiteAccess sets the Site access status of the account identified by the account email; if no such account exists, an error is returned.
|
||||
func (mngr *AccountsManager) GrantSiteAccess(accountData *data.Account, grantAccess bool) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
account, err := mngr.findAccount(FindByEmail, accountData.Email)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "no account with the specified email exists")
|
||||
}
|
||||
|
||||
return mngr.grantAccess(account, &account.Data.SiteAccess, grantAccess, email.SendSiteAccessGranted)
|
||||
}
|
||||
|
||||
// GrantGOCDBAccess sets the GOCDB access status of the account identified by the account email; if no such account exists, an error is returned.
|
||||
func (mngr *AccountsManager) GrantGOCDBAccess(accountData *data.Account, grantAccess bool) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
account, err := mngr.findAccount(FindByEmail, accountData.Email)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "no account with the specified email exists")
|
||||
}
|
||||
|
||||
return mngr.grantAccess(account, &account.Data.GOCDBAccess, grantAccess, email.SendGOCDBAccessGranted)
|
||||
}
|
||||
|
||||
// RemoveAccount removes the account identified by the account email; if no such account exists, an error is returned.
|
||||
func (mngr *AccountsManager) RemoveAccount(accountData *data.Account) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
for i, account := range mngr.accounts {
|
||||
if strings.EqualFold(account.Email, accountData.Email) {
|
||||
mngr.accounts = append(mngr.accounts[:i], mngr.accounts[i+1:]...)
|
||||
mngr.storage.AccountRemoved(account)
|
||||
mngr.writeAllAccounts()
|
||||
|
||||
mngr.callListeners(account, AccountsListener.AccountRemoved)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Errorf("no account with the specified email exists")
|
||||
}
|
||||
|
||||
// SendContactForm sends a generic email to the ScienceMesh admins.
|
||||
func (mngr *AccountsManager) SendContactForm(account *data.Account, subject, message string) {
|
||||
mngr.sendEmail(account, map[string]string{"Subject": subject, "Message": message}, email.SendContactForm)
|
||||
}
|
||||
|
||||
// CloneAccounts retrieves all accounts currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible.
|
||||
func (mngr *AccountsManager) CloneAccounts(erasePasswords bool) data.Accounts {
|
||||
mngr.mutex.RLock()
|
||||
defer mngr.mutex.RUnlock()
|
||||
|
||||
clones := make(data.Accounts, 0, len(mngr.accounts))
|
||||
for _, acc := range mngr.accounts {
|
||||
clones = append(clones, acc.Clone(erasePasswords))
|
||||
}
|
||||
|
||||
return clones
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) grantAccess(account *data.Account, accessFlag *bool, grantAccess bool, emailFunc email.SendFunction) error {
|
||||
accessOld := *accessFlag
|
||||
*accessFlag = grantAccess
|
||||
|
||||
mngr.storage.AccountUpdated(account)
|
||||
mngr.writeAllAccounts()
|
||||
|
||||
if *accessFlag && *accessFlag != accessOld {
|
||||
mngr.sendEmail(account, nil, emailFunc)
|
||||
}
|
||||
|
||||
mngr.callListeners(account, AccountsListener.AccountUpdated)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) callListeners(account *data.Account, cb AccountsListenerCallback) {
|
||||
for _, listener := range mngr.accountsListeners {
|
||||
cb(listener, account)
|
||||
}
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) sendEmail(account *data.Account, params map[string]string, sendFunc email.SendFunction) {
|
||||
_ = sendFunc(account, []string{account.Email, mngr.conf.Email.NotificationsMail}, params, *mngr.conf)
|
||||
}
|
||||
|
||||
// NewAccountsManager creates a new accounts manager instance.
|
||||
func NewAccountsManager(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) (*AccountsManager, error) {
|
||||
mngr := &AccountsManager{}
|
||||
if err := mngr.initialize(storage, conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the accounts manager")
|
||||
}
|
||||
return mngr, nil
|
||||
}
|
||||
Generated
Vendored
+97
@@ -0,0 +1,97 @@
|
||||
// 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 gocdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/mentix/utils/network"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
opCreateOrUpdate = "CreateOrUpdate"
|
||||
opDelete = "Delete"
|
||||
)
|
||||
|
||||
type writeAccountUserData struct {
|
||||
Email string `json:"Email"`
|
||||
FirstName string `json:"FirstName"`
|
||||
LastName string `json:"LastName"`
|
||||
PhoneNumber string `json:"PhoneNumber"`
|
||||
}
|
||||
|
||||
type writeAccountData struct {
|
||||
APIKey string `json:"APIKey"`
|
||||
Operation string `json:"Operation"`
|
||||
|
||||
Data writeAccountUserData `json:"Data"`
|
||||
}
|
||||
|
||||
func writeAccount(account *data.Account, operation string, address string, apiKey string) error {
|
||||
// Fill in the data to send
|
||||
userData := getWriteAccountData(account)
|
||||
userData.APIKey = apiKey
|
||||
userData.Operation = operation
|
||||
|
||||
// Send the data to the GOCDB endpoint
|
||||
endpointURL, err := network.GenerateURL(address, "/ext/v1/user", network.URLParams{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to generate the GOCDB URL")
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(userData)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to marshal the user data")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, endpointURL.String(), bytes.NewReader(jsonData))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create HTTP request")
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to send data to endpoint")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
msg, _ := io.ReadAll(resp.Body)
|
||||
return errors.Errorf("unable to perform request: %v", string(msg))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getWriteAccountData(account *data.Account) *writeAccountData {
|
||||
return &writeAccountData{
|
||||
Data: writeAccountUserData{
|
||||
Email: account.Email,
|
||||
FirstName: account.FirstName,
|
||||
LastName: account.LastName,
|
||||
PhoneNumber: account.PhoneNumber,
|
||||
},
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// 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 gocdb
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// AccountsListener is the GOCDB accounts listener.
|
||||
type AccountsListener struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func (listener *AccountsListener) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
listener.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
listener.log = log
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AccountCreated is called whenever an account was created.
|
||||
func (listener *AccountsListener) AccountCreated(account *data.Account) {
|
||||
listener.updateGOCDB(account, false)
|
||||
}
|
||||
|
||||
// AccountUpdated is called whenever an account was updated.
|
||||
func (listener *AccountsListener) AccountUpdated(account *data.Account) {
|
||||
listener.updateGOCDB(account, false)
|
||||
}
|
||||
|
||||
// AccountRemoved is called whenever an account was removed.
|
||||
func (listener *AccountsListener) AccountRemoved(account *data.Account) {
|
||||
listener.updateGOCDB(account, true)
|
||||
}
|
||||
|
||||
func (listener *AccountsListener) updateGOCDB(account *data.Account, forceRemoval bool) {
|
||||
if account != nil && account.Data.GOCDBAccess && !forceRemoval {
|
||||
if err := writeAccount(account, opCreateOrUpdate, listener.conf.GOCDB.WriteURL, listener.conf.GOCDB.APIKey); err != nil {
|
||||
listener.log.Err(err).Str("userid", account.Email).Msg("unable to update GOCDB account")
|
||||
}
|
||||
} else {
|
||||
// Errors while deleting an account are ignored (account might not exist at all, for example)
|
||||
_ = writeAccount(account, opDelete, listener.conf.GOCDB.WriteURL, listener.conf.GOCDB.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
// NewListener creates a new GOCDB accounts listener.
|
||||
func NewListener(conf *config.Configuration, log *zerolog.Logger) (*AccountsListener, error) {
|
||||
listener := &AccountsListener{}
|
||||
if err := listener.initialize(conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the GOCDB accounts listener")
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
Generated
Vendored
+185
@@ -0,0 +1,185 @@
|
||||
// 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 manager
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// SitesManager is responsible for all sites related tasks.
|
||||
type SitesManager struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
storage data.Storage
|
||||
|
||||
sites data.Sites
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) initialize(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
mngr.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
mngr.log = log
|
||||
|
||||
if storage == nil {
|
||||
return errors.Errorf("no storage provided")
|
||||
}
|
||||
mngr.storage = storage
|
||||
|
||||
mngr.sites = make(data.Sites, 0, 32) // Reserve some space for sites
|
||||
mngr.readAllSites()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) readAllSites() {
|
||||
if sites, err := mngr.storage.ReadSites(); err == nil {
|
||||
mngr.sites = *sites
|
||||
} else {
|
||||
// Just warn when not being able to read sites
|
||||
mngr.log.Warn().Err(err).Msg("error while reading sites")
|
||||
}
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) writeAllSites() {
|
||||
if err := mngr.storage.WriteSites(&mngr.sites); err != nil {
|
||||
// Just warn when not being able to write sites
|
||||
mngr.log.Warn().Err(err).Msg("error while writing sites")
|
||||
}
|
||||
}
|
||||
|
||||
// GetSite retrieves the site with the given ID, creating it first if necessary.
|
||||
func (mngr *SitesManager) GetSite(id string, cloneSite bool) (*data.Site, error) {
|
||||
mngr.mutex.RLock()
|
||||
defer mngr.mutex.RUnlock()
|
||||
|
||||
site, err := mngr.getSite(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cloneSite {
|
||||
site = site.Clone(false)
|
||||
}
|
||||
|
||||
return site, nil
|
||||
}
|
||||
|
||||
// FindSite returns the site specified by the ID if one exists.
|
||||
func (mngr *SitesManager) FindSite(id string) *data.Site {
|
||||
site, _ := mngr.findSite(id)
|
||||
return site
|
||||
}
|
||||
|
||||
// UpdateSite updates the site identified by the site ID; if no such site exists, one will be created first.
|
||||
func (mngr *SitesManager) UpdateSite(siteData *data.Site) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
site, err := mngr.getSite(siteData.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "site to update not found")
|
||||
}
|
||||
|
||||
if err := site.Update(siteData, mngr.conf.Security.CredentialsPassphrase); err == nil {
|
||||
mngr.storage.SiteUpdated(site)
|
||||
mngr.writeAllSites()
|
||||
} else {
|
||||
return errors.Wrap(err, "error while updating site")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneSites retrieves all sites currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible.
|
||||
func (mngr *SitesManager) CloneSites(eraseCredentials bool) data.Sites {
|
||||
mngr.mutex.RLock()
|
||||
defer mngr.mutex.RUnlock()
|
||||
|
||||
clones := make(data.Sites, 0, len(mngr.sites))
|
||||
for _, site := range mngr.sites {
|
||||
clones = append(clones, site.Clone(eraseCredentials))
|
||||
}
|
||||
|
||||
return clones
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) getSite(id string) (*data.Site, error) {
|
||||
site, err := mngr.findSite(id)
|
||||
if site == nil {
|
||||
site, err = mngr.createSite(id)
|
||||
}
|
||||
return site, err
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) createSite(id string) (*data.Site, error) {
|
||||
site, err := data.NewSite(id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error while creating site")
|
||||
}
|
||||
mngr.sites = append(mngr.sites, site)
|
||||
mngr.storage.SiteAdded(site)
|
||||
mngr.writeAllSites()
|
||||
return site, nil
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) findSite(id string) (*data.Site, error) {
|
||||
if len(id) == 0 {
|
||||
return nil, errors.Errorf("no search ID specified")
|
||||
}
|
||||
|
||||
site := mngr.findSiteByPredicate(func(site *data.Site) bool { return strings.EqualFold(site.ID, id) })
|
||||
if site != nil {
|
||||
return site, nil
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("no site found matching the specified ID")
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) findSiteByPredicate(predicate func(*data.Site) bool) *data.Site {
|
||||
for _, site := range mngr.sites {
|
||||
if predicate(site) {
|
||||
return site
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewSitesManager creates a new sites manager instance.
|
||||
func NewSitesManager(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) (*SitesManager, error) {
|
||||
mngr := &SitesManager{}
|
||||
if err := mngr.initialize(storage, conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the sites manager")
|
||||
}
|
||||
return mngr, nil
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// 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 manager
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
)
|
||||
|
||||
type userToken struct {
|
||||
jwt.RegisteredClaims
|
||||
|
||||
User string `json:"user"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
const (
|
||||
tokenKeyLength = 16
|
||||
tokenIssuer = "sciencemesh_siteacc"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenSecret string
|
||||
)
|
||||
|
||||
func generateUserToken(user string, scope string, timeout int) (string, error) {
|
||||
// Create a JWT as the user token
|
||||
claims := userToken{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(timeout) * time.Second)),
|
||||
Issuer: tokenIssuer,
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
User: user,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims)
|
||||
signedToken, err := token.SignedString([]byte(tokenSecret))
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error signing token with claims %+v", claims)
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
func extractUserToken(token string) (*userToken, error) {
|
||||
// Parse the token and try to extract the claims
|
||||
parsedToken, err := jwt.ParseWithClaims(token, &userToken{}, func(token *jwt.Token) (interface{}, error) { return []byte(tokenSecret), nil })
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing token")
|
||||
}
|
||||
|
||||
if claims, ok := parsedToken.Claims.(*userToken); ok && parsedToken.Valid {
|
||||
if claims.Issuer != tokenIssuer {
|
||||
return nil, errors.Errorf("invalid token issuer")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("invalid token")
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Generate the token secret randomly
|
||||
tokenSecret = password.MustGenerate(tokenKeyLength, tokenKeyLength/4, tokenKeyLength/4, false, true)
|
||||
}
|
||||
Generated
Vendored
+150
@@ -0,0 +1,150 @@
|
||||
// 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 manager
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// UsersManager is responsible for managing logged in users through session objects.
|
||||
type UsersManager struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
sitesManager *SitesManager
|
||||
accountsManager *AccountsManager
|
||||
}
|
||||
|
||||
const (
|
||||
defaultPasswordLength = 12
|
||||
)
|
||||
|
||||
func (mngr *UsersManager) initialize(conf *config.Configuration, log *zerolog.Logger, sitesManager *SitesManager, accountsManager *AccountsManager) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
mngr.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
mngr.log = log
|
||||
|
||||
if sitesManager == nil {
|
||||
return errors.Errorf("no sites manager provided")
|
||||
}
|
||||
mngr.sitesManager = sitesManager
|
||||
|
||||
if accountsManager == nil {
|
||||
return errors.Errorf("no accounts manager provided")
|
||||
}
|
||||
mngr.accountsManager = accountsManager
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoginUser tries to login a given username/password pair. On success, the corresponding user account is stored in the session and a user token is returned.
|
||||
func (mngr *UsersManager) LoginUser(name, password string, scope string, session *html.Session) (string, error) {
|
||||
account, err := mngr.accountsManager.FindAccountEx(FindByEmail, name, false)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "no account with the specified email exists")
|
||||
}
|
||||
|
||||
// Verify the provided password
|
||||
if !account.Password.Compare(password) {
|
||||
return "", errors.Errorf("invalid password")
|
||||
}
|
||||
|
||||
// Check if the user has access to the specified scope
|
||||
if !account.CheckScopeAccess(scope) {
|
||||
return "", errors.Errorf("no access to the specified scope granted")
|
||||
}
|
||||
|
||||
// Get the site the account belongs to
|
||||
site, err := mngr.sitesManager.GetSite(account.Site, false)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "no site with the specified ID exists")
|
||||
}
|
||||
|
||||
// Store the user account in the session
|
||||
session.LoginUser(account, site)
|
||||
|
||||
// Generate a token that can be used as a "ticket"
|
||||
token, err := generateUserToken(session.LoggedInUser().Account.Email, scope, mngr.conf.Webserver.SessionTimeout)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to generate user token")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// LogoutUser logs the current user out.
|
||||
func (mngr *UsersManager) LogoutUser(session *html.Session) {
|
||||
// Just unset the user account stored in the session
|
||||
session.LogoutUser()
|
||||
}
|
||||
|
||||
// VerifyUserToken is used to verify a user token against the current session.
|
||||
func (mngr *UsersManager) VerifyUserToken(token string, user string, scope string) (string, error) {
|
||||
// Verify the token by trying to extract it
|
||||
utoken, err := extractUserToken(token)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to verify user token")
|
||||
}
|
||||
|
||||
// Check the provided email against the stored one
|
||||
if !strings.EqualFold(utoken.User, user) {
|
||||
return "", errors.Errorf("mismatching user")
|
||||
}
|
||||
|
||||
// Check if the user account actually exists and has proper scope access
|
||||
if strings.EqualFold(scope, utoken.Scope) {
|
||||
if acc, err := mngr.accountsManager.FindAccount(FindByEmail, utoken.User); err == nil {
|
||||
if !acc.CheckScopeAccess(scope) {
|
||||
return "", errors.Errorf("no scope access")
|
||||
}
|
||||
} else {
|
||||
return "", errors.Errorf("invalid email")
|
||||
}
|
||||
} else {
|
||||
return "", errors.Errorf("invalid scope")
|
||||
}
|
||||
|
||||
// Refresh the user token (as a form of keep-alive, since tokens expire quickly)
|
||||
newToken, err := generateUserToken(utoken.User, utoken.Scope, mngr.conf.Webserver.SessionTimeout)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to refresh user token")
|
||||
}
|
||||
|
||||
return newToken, nil
|
||||
}
|
||||
|
||||
// NewUsersManager creates a new users manager instance.
|
||||
func NewUsersManager(conf *config.Configuration, log *zerolog.Logger, sitesManager *SitesManager, accountsManager *AccountsManager) (*UsersManager, error) {
|
||||
mngr := &UsersManager{}
|
||||
if err := mngr.initialize(conf, log, sitesManager, accountsManager); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the users manager")
|
||||
}
|
||||
return mngr, nil
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
// Copyright 2018-2021 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 siteacc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
accpanel "github.com/opencloud-eu/reva/v2/pkg/siteacc/account"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/admin"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/alerting"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/manager"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// SiteAccounts represents the main Site Accounts service object.
|
||||
type SiteAccounts struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
sessions *html.SessionManager
|
||||
|
||||
storage data.Storage
|
||||
|
||||
sitesManager *manager.SitesManager
|
||||
accountsManager *manager.AccountsManager
|
||||
usersManager *manager.UsersManager
|
||||
|
||||
alertsDispatcher *alerting.Dispatcher
|
||||
|
||||
adminPanel *admin.Panel
|
||||
accountPanel *accpanel.Panel
|
||||
}
|
||||
|
||||
func (siteacc *SiteAccounts) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return fmt.Errorf("no configuration provided")
|
||||
}
|
||||
siteacc.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return fmt.Errorf("no logger provided")
|
||||
}
|
||||
siteacc.log = log
|
||||
|
||||
// Create the session mananger
|
||||
sessions, err := html.NewSessionManager("siteacc_session", conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error while creating the session manager")
|
||||
}
|
||||
siteacc.sessions = sessions
|
||||
|
||||
// Create the central storage
|
||||
storage, err := siteacc.createStorage(conf.Storage.Driver)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create storage")
|
||||
}
|
||||
siteacc.storage = storage
|
||||
|
||||
// Create the sites manager instance
|
||||
smngr, err := manager.NewSitesManager(storage, conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating the sites manager")
|
||||
}
|
||||
siteacc.sitesManager = smngr
|
||||
|
||||
// Create the accounts manager instance
|
||||
amngr, err := manager.NewAccountsManager(storage, conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating the accounts manager")
|
||||
}
|
||||
siteacc.accountsManager = amngr
|
||||
|
||||
// Create the users manager instance
|
||||
umngr, err := manager.NewUsersManager(conf, log, siteacc.sitesManager, siteacc.accountsManager)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating the users manager")
|
||||
}
|
||||
siteacc.usersManager = umngr
|
||||
|
||||
// Create the alerts dispatcher instance
|
||||
dispatcher, err := alerting.NewDispatcher(conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating the alerts dispatcher")
|
||||
}
|
||||
siteacc.alertsDispatcher = dispatcher
|
||||
|
||||
// Create the admin panel
|
||||
if pnl, err := admin.NewPanel(conf, log); err == nil {
|
||||
siteacc.adminPanel = pnl
|
||||
} else {
|
||||
return errors.Wrap(err, "unable to create the administration panel")
|
||||
}
|
||||
|
||||
// Create the account panel
|
||||
if pnl, err := accpanel.NewPanel(conf, log); err == nil {
|
||||
siteacc.accountPanel = pnl
|
||||
} else {
|
||||
return errors.Wrap(err, "unable to create the account panel")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestHandler returns the HTTP request handler of the service.
|
||||
func (siteacc *SiteAccounts) RequestHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
// Get the active session for the request (or create a new one); a valid session object will always be returned
|
||||
siteacc.sessions.PurgeSessions() // Remove expired sessions first
|
||||
session, err := siteacc.sessions.HandleRequest(w, r)
|
||||
if err != nil {
|
||||
siteacc.log.Err(err).Msg("an error occurred while handling sessions")
|
||||
}
|
||||
|
||||
epHandled := false
|
||||
for _, ep := range getEndpoints() {
|
||||
if ep.Path == r.URL.Path {
|
||||
ep.Handler(siteacc, ep, w, r, session)
|
||||
epHandled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !epHandled {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = fmt.Fprintf(w, "Unknown endpoint %v", r.URL.Path)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ShowAdministrationPanel writes the administration panel HTTP output directly to the response writer.
|
||||
func (siteacc *SiteAccounts) ShowAdministrationPanel(w http.ResponseWriter, r *http.Request, session *html.Session) error {
|
||||
// The admin panel only shows the stored accounts and offers actions through links, so let it use cloned data
|
||||
accounts := siteacc.accountsManager.CloneAccounts(true)
|
||||
return siteacc.adminPanel.Execute(w, r, session, &accounts)
|
||||
}
|
||||
|
||||
// ShowAccountPanel writes the account panel HTTP output directly to the response writer.
|
||||
func (siteacc *SiteAccounts) ShowAccountPanel(w http.ResponseWriter, r *http.Request, session *html.Session) error {
|
||||
return siteacc.accountPanel.Execute(w, r, session)
|
||||
}
|
||||
|
||||
// SitesManager returns the central sites manager instance.
|
||||
func (siteacc *SiteAccounts) SitesManager() *manager.SitesManager {
|
||||
return siteacc.sitesManager
|
||||
}
|
||||
|
||||
// AccountsManager returns the central accounts manager instance.
|
||||
func (siteacc *SiteAccounts) AccountsManager() *manager.AccountsManager {
|
||||
return siteacc.accountsManager
|
||||
}
|
||||
|
||||
// UsersManager returns the central users manager instance.
|
||||
func (siteacc *SiteAccounts) UsersManager() *manager.UsersManager {
|
||||
return siteacc.usersManager
|
||||
}
|
||||
|
||||
// AlertsDispatcher returns the central alerts dispatcher instance.
|
||||
func (siteacc *SiteAccounts) AlertsDispatcher() *alerting.Dispatcher {
|
||||
return siteacc.alertsDispatcher
|
||||
}
|
||||
|
||||
// GetPublicEndpoints returns a list of all public endpoints.
|
||||
func (siteacc *SiteAccounts) GetPublicEndpoints() []string {
|
||||
// TODO: Only for local testing!
|
||||
// return []string{"/"}
|
||||
|
||||
endpoints := make([]string, 0, 5)
|
||||
for _, ep := range getEndpoints() {
|
||||
if ep.IsPublic {
|
||||
endpoints = append(endpoints, ep.Path)
|
||||
}
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func (siteacc *SiteAccounts) createStorage(driver string) (data.Storage, error) {
|
||||
if driver == "file" {
|
||||
return data.NewFileStorage(siteacc.conf, siteacc.log)
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("unknown storage driver %v", driver)
|
||||
}
|
||||
|
||||
// New returns a new Site Accounts service instance.
|
||||
func New(conf *config.Configuration, log *zerolog.Logger) (*SiteAccounts, error) {
|
||||
// Configure the accounts service
|
||||
siteacc := new(SiteAccounts)
|
||||
if err := siteacc.initialize(conf, log); err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize site accounts: %v", err)
|
||||
}
|
||||
return siteacc, nil
|
||||
}
|
||||
Reference in New Issue
Block a user