Skip to content

Commit

Permalink
Allow to protect shares by link with a password (#4240)
Browse files Browse the repository at this point in the history
  • Loading branch information
nono authored Dec 5, 2023
2 parents 39a20d3 + 84dc60b commit 4281dc5
Show file tree
Hide file tree
Showing 23 changed files with 1,414 additions and 919 deletions.
6 changes: 6 additions & 0 deletions assets/images/security.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions assets/locales/de.po
Original file line number Diff line number Diff line change
Expand Up @@ -969,3 +969,15 @@ msgstr "Install the Cozy app"

msgid "Install flagship app Skip"
msgstr "Continue in the browser"

msgid "Share by link Password Title"
msgstr "Enter the password to access the link"

msgid "Share by link Password Field"
msgstr "Password"

msgid "Share by link Password Submit"
msgstr "Continue"

msgid "Share by link Password Invalid"
msgstr "Invalid password"
12 changes: 12 additions & 0 deletions assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -1256,3 +1256,15 @@ msgstr "Install the Cozy app"

msgid "Install flagship app Skip"
msgstr "Continue in the browser"

msgid "Share by link Password Title"
msgstr "Enter the password to access the link"

msgid "Share by link Password Field"
msgstr "Password"

msgid "Share by link Password Submit"
msgstr "Continue"

msgid "Share by link Password Invalid"
msgstr "Invalid password"
12 changes: 12 additions & 0 deletions assets/locales/es.po
Original file line number Diff line number Diff line change
Expand Up @@ -980,3 +980,15 @@ msgstr "Install the Cozy app"

msgid "Install flagship app Skip"
msgstr "Continue in the browser"

msgid "Share by link Password Title"
msgstr "Enter the password to access the link"

msgid "Share by link Password Field"
msgstr "Password"

msgid "Share by link Password Submit"
msgstr "Continue"

msgid "Share by link Password Invalid"
msgstr "Invalid password"
12 changes: 12 additions & 0 deletions assets/locales/fr.po
Original file line number Diff line number Diff line change
Expand Up @@ -1372,3 +1372,15 @@ msgstr "Installer l'app Cozy"

msgid "Install flagship app Skip"
msgstr "Continuer dans mon navigateur"

msgid "Share by link Password Title"
msgstr "Entrer le mot de passe pour acccéder au lien"

msgid "Share by link Password Field"
msgstr "Mot de passe"

msgid "Share by link Password Submit"
msgstr "Continuer"

msgid "Share by link Password Invalid"
msgstr "Mot de passe incorrect"
12 changes: 12 additions & 0 deletions assets/locales/ja.po
Original file line number Diff line number Diff line change
Expand Up @@ -784,3 +784,15 @@ msgstr "Install the Cozy app"

msgid "Install flagship app Skip"
msgstr "Continue in the browser"

msgid "Share by link Password Title"
msgstr "Enter the password to access the link"

msgid "Share by link Password Field"
msgstr "Password"

msgid "Share by link Password Submit"
msgstr "Continue"

msgid "Share by link Password Invalid"
msgstr "Invalid password"
12 changes: 12 additions & 0 deletions assets/locales/nl_NL.po
Original file line number Diff line number Diff line change
Expand Up @@ -1165,3 +1165,15 @@ msgstr "Install the Cozy app"

msgid "Install flagship app Skip"
msgstr "Continue in the browser"

msgid "Share by link Password Title"
msgstr "Enter the password to access the link"

msgid "Share by link Password Field"
msgstr "Password"

msgid "Share by link Password Submit"
msgstr "Continue"

msgid "Share by link Password Invalid"
msgstr "Invalid password"
49 changes: 49 additions & 0 deletions assets/scripts/share-by-link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
(function (w, d) {
if (!w.fetch || !w.Headers) return

const form = d.getElementById('share-by-link-password-form')
const field = d.getElementById('password-field')
const submit = d.getElementById('password-submit')
const input = d.getElementById('password')
const permID = d.getElementById('perm-id')

const onSubmit = function (event) {
event.preventDefault()
input.setAttribute('disabled', true)
submit.setAttribute('disabled', true)

const data = new URLSearchParams()
data.append('password', input.value)
data.append('perm_id', permID.value)

const headers = new Headers()
headers.append('Content-Type', 'application/x-www-form-urlencoded')

return fetch(form.action, {
method: 'POST',
headers: headers,
body: data,
credentials: 'include',
})
.then((response) => {
if (response.status < 400) {
const tooltip = field.querySelector('.invalid-tooltip')
if (tooltip) {
tooltip.classList.add('d-none')
}
submit.innerHTML = '<span class="icon icon-check"></span>'
submit.classList.add('btn-done')
w.location.reload()
} else {
return response.json().then(function (body) {
w.showError(field, body.error)
input.removeAttribute('disabled')
submit.removeAttribute('disabled')
})
}
})
.catch((err) => w.showError(field, err))
}

form.addEventListener('submit', onSubmit)
})(window, document)
55 changes: 55 additions & 0 deletions assets/templates/share_by_link_password.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="{{.Locale}}">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="3600">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#fff">
<title>{{.Title}}</title>
<link rel="stylesheet" href="{{asset .Domain "/fonts/fonts.css" .ContextName}}">
<link rel="stylesheet" href="{{asset .Domain "/css/cozy-bs.min.css" .ContextName}}">
<link rel="stylesheet" href="{{asset .Domain "/styles/theme.css" .ContextName}}">
<link rel="stylesheet" href="{{asset .Domain "/styles/cirrus.css" .ContextName}}">
{{.Favicon}}
</head>
<body class="cirrus theme-inverted">
<form id="share-by-link-password-form" method="POST" action="{{.Action}}" class="d-contents">
<main class="wrapper">

<header class="wrapper-top">
<a href="https://cozy.io/" class="btn p-2 d-sm-none">
<img src="{{asset .Domain "/images/logo-dark.svg"}}" alt="Cozy Cloud" class="logo" />
</a>
</header>

<div class="d-flex flex-column align-items-center">
<img src="{{asset .Domain "/images/security.svg"}}" alt="" class="illustration mb-3" />
<h1 class="h4 h2-md mb-3 text-center">{{t "Share by link Password Title"}}</h1>

<div id="password-field" class="input-group form-floating has-validation w-100">
<input id="perm-id" type="hidden" name="perm_id" value="{{.PermID}}" />
<input type="password" class="form-control form-control-md-lg" id="password" name="passphrase" autofocus spellcheck="false" />
<label for="password">{{t "Share by link Password Field"}}</label>
<button id="password-visibility-button" class="btn btn-outline-info"
type="button"
name="password-visibility"
data-show="{{t "Login Password show"}}"
data-hide="{{t "Login Password hide"}}"
title="{{t "Login Password show"}}">
<span id="password-visibility-icon" class="icon icon-eye-closed"></span>
</button>
</div>
</div>

<footer class="w-100">
<button id="password-submit" class="btn btn-primary btn-md-lg w-100 my-3 mt-md-5" type="submit">
{{t "Share by link Password Submit"}}
</button>
</footer>

</main>
</form>
<script src="{{asset .Domain "/scripts/cirrus.js"}}"></script>
<script src="{{asset .Domain "/scripts/share-by-link.js"}}"></script>
</body>
</html>
1 change: 1 addition & 0 deletions docs/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ with given parameter. For example:
* http://cozy.localhost:8080/dev/templates/oidc_twofactor.html
* http://cozy.localhost:8080/dev/templates/passphrase_choose.html
* http://cozy.localhost:8080/dev/templates/passphrase_reset.html?ShowBackButton=true&HasCiphers=true&HasHint=true
* http://cozy.localhost:8080/dev/templates/share_by_link_password.html
* http://cozy.localhost:8080/dev/templates/sharing_discovery.html?PublicName=Jane&RecipientDomain=mycozy.cloud&NotEmailError=true
* http://cozy.localhost:8080/dev/templates/oauth_clients_limit_exceeded.html
* http://cozy.localhost:8080/dev/templates/twofactor.html?TrustedDeviceCheckBox=true
Expand Down
30 changes: 30 additions & 0 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,36 @@ Content-Type: application/json
"OWY0MjNjMGEtOTNmNi0xMWVjLWIyZGItN2I5YjgwNmRjYzBiCg"
```

### POST /auth/share-by-link/password

This route is used when a share by link is protected by password. The password
can be sent to this route to create a cookie that will allow to use the
sharecode and access the shared page.

#### Request

```http
POST /auth/clients/share-by-link/password HTTP/1.1
Host: cozy.example.org
Content-Type: application/x-www-form-urlencoded
password=HelloWorld!&perm_id=123456789
```

#### Response

```http
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: pass123...
```

```json
{
"password": "ok"
}
```

### FAQ

> What format is used for tokens?
Expand Down
3 changes: 3 additions & 0 deletions docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ Accept: application/vnd.api+json
"type": "io.cozy.permissions",
"attributes": {
"source_id": "io.cozy.apps/my-awesome-game",
"password": "HelloWorld!",
"permissions": {
"images": {
"type": "io.cozy.files",
Expand Down Expand Up @@ -341,6 +342,7 @@ Content-Type: application/vnd.api+json
"jane": "123456aBCdef"
},
"expires_at": 1483951978,
"password": true,
"permissions": {
"images": {
"type": "io.cozy.files",
Expand Down Expand Up @@ -390,6 +392,7 @@ Content-Type: application/vnd.api+json
"jane": "123456aBCdef"
},
"expires_at": 1483951978,
"password": true,
"permissions": {
"images": {
"type": "io.cozy.files",
Expand Down
12 changes: 12 additions & 0 deletions model/permission/permissions.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Package permission is used to store the permissions for each webapp,
// konnector, sharing, etc.
package permission

import (
Expand All @@ -11,6 +13,7 @@ import (
"github.com/cozy/cozy-stack/pkg/consts"
"github.com/cozy/cozy-stack/pkg/couchdb"
"github.com/cozy/cozy-stack/pkg/couchdb/mango"
"github.com/cozy/cozy-stack/pkg/crypto"
"github.com/cozy/cozy-stack/pkg/metadata"
"github.com/cozy/cozy-stack/pkg/prefixer"
"github.com/labstack/echo/v4"
Expand All @@ -31,6 +34,7 @@ type Permission struct {
ExpiresAt *time.Time `json:"expires_at,omitempty"`
Codes map[string]string `json:"codes,omitempty"`
ShortCodes map[string]string `json:"shortcodes,omitempty"`
Password interface{} `json:"password,omitempty"`

Client interface{} `json:"-"` // Contains the *oauth.Client client pointer for Oauth permission type
Metadata *metadata.CozyMetadata `json:"cozyMetadata,omitempty"`
Expand Down Expand Up @@ -507,6 +511,14 @@ func CreateShareSet(db prefixer.Prefixer, parent *Permission, sourceID string, c
Metadata: subdoc.Metadata,
}

if pass, ok := subdoc.Password.(string); ok && len(pass) > 0 {
hash, err := crypto.GenerateFromPassphrase([]byte(pass))
if err != nil {
return nil, err
}
doc.Password = hash
}

err := couchdb.CreateDoc(db, doc)
if err != nil {
return nil, err
Expand Down
30 changes: 29 additions & 1 deletion web/apps/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ func handleIntent(c echo.Context, i *instance.Instance, slug, intentID string) {
return
}
from := i.SubDomain(parts[1]).String()
middlewares.AppendCSPRule(c, "frame-ancestors", from)
if !config.GetConfig().CSPDisabled {
middlewares.AppendCSPRule(c, "frame-ancestors", from)
}
}

// ServeAppFile will serve the requested file using the specified application
Expand Down Expand Up @@ -248,6 +250,19 @@ func ServeAppFile(c echo.Context, i *instance.Instance, fs appfs.FileServer, web
}
}

// For share by link, show the password page if it is password protected.
code := c.QueryParam("sharecode")
token, err := middlewares.TransformShortcodeToJWT(i, code)
if err == nil {
claims, err := middlewares.ExtractClaims(c, i, token)
if err == nil && claims.AudienceString() == consts.ShareAudience {
pdoc, err := permission.GetForShareCode(i, token)
if err == nil && pdoc.Password != nil && !middlewares.HasCookieForPassword(c, i, pdoc.ID()) {
return renderPasswordPage(c, i, pdoc.ID())
}
}
}

if intentID := c.QueryParam("intent"); intentID != "" {
handleIntent(c, i, slug, intentID)
}
Expand Down Expand Up @@ -437,6 +452,19 @@ func renderMovedLink(c echo.Context, i *instance.Instance, to, subdomainType str
})
}

func renderPasswordPage(c echo.Context, inst *instance.Instance, permID string) error {
return c.Render(http.StatusUnauthorized, "share_by_link_password.html", echo.Map{
"Action": inst.PageURL("/auth/share-by-link/password", nil),
"Domain": inst.ContextualDomain(),
"ContextName": inst.ContextName,
"Locale": inst.Locale,
"Title": inst.TemplateTitle(),
"ThemeCSS": middlewares.ThemeCSS(inst),
"Favicon": middlewares.Favicon(inst),
"PermID": permID,
})
}

// serveParams is a struct used for rendering the index.html of webapps. A
// struct is used, and not a map, to have some methods declared on it. It
// allows to be lazy when constructing the paths of the assets: if an asset is
Expand Down
3 changes: 3 additions & 0 deletions web/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,4 +635,7 @@ func Routes(router *echo.Group) {
// 2FA
router.GET("/twofactor", twoFactorForm)
router.POST("/twofactor", twoFactor)

// Share by link protected by password
router.POST("/share-by-link/password", checkPasswordForShareByLink)
}
Loading

0 comments on commit 4281dc5

Please sign in to comment.