Skip to content

Commit

Permalink
Merge pull request #1210 from solid/sharingPatch
Browse files Browse the repository at this point in the history
Sharing patch
  • Loading branch information
michielbdejong authored Jun 4, 2019
2 parents bdc5acb + f21df34 commit 4d36e12
Show file tree
Hide file tree
Showing 15 changed files with 478 additions and 242 deletions.
44 changes: 0 additions & 44 deletions common/disclaimer.html

This file was deleted.

48 changes: 0 additions & 48 deletions default-views/auth/consent.hbs

This file was deleted.

49 changes: 49 additions & 0 deletions default-views/auth/sharing.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{title}}</title>
<!-- Bootstrap CSS and Theme for demo purposes -->
<link rel="stylesheet" href="/common/css/bootstrap.min.css">
<link rel="stylesheet" href="/common/css/solid.css">
</head>
<body>
<div class="container title">
<h1>Authorize {{app_origin}} to access your Pod?</h1>
<p>Solid allows you to precisely choose what other people and apps can read and write in a Pod. This version of the authorization user interface (node-solid-server V5.1) only supports the toggle of global access permissions to all of the data in your Pod.</p>
<p><strong>If you don’t want to set these permissions at a global level, uncheck all of the boxes below, then click authorize.</strong> This will add the application origin to your authorization list, without granting it permission to any of your data yet. You will then need to manage those permissions yourself by setting them explicitly in the places you want this application to access.</p>
<div class="panel panel-default">
<div class="panel-body">
<div class="page-title">
<p>By clicking Authorize, any app from {{app_origin}} will be able to:</p>
</div>
<form method="post" action="/sharing">

<input id="read" type="checkbox" name="access_mode" value="Read" checked>
<label for="read">Read all documents in the Pod</label>
<br>

<input id="write" type="checkbox" name="access_mode" value="Write" checked>
<label for="write">Add data to existing documents, and create new documents</label>
<br>

<input id="append" type="checkbox" name="access_mode" value="Append" checked>
<label for="append">Modify and delete data in existing documents, and delete documents</label>
<br>

<input id="control" type="checkbox" name="access_mode" value="Control">
<label for="control">Give other people and apps access to the Pod, or revoke their (and your) access</label>
<br>
<br>

<button type="submit" class="btn btn-primary" name="consent" value="true">Authorize</button>
<button type="submit" class="btn btn-default" name="cancel" value="true">Cancel</button>
{{> auth/auth-hidden-fields}}
</form>
</div>
</div>
<p><i>This server (node-solid-server V5.1) only implements a limited subset of OpenID Connect, and doesn’t yet support token issuance for applications. OIDC Token Issuance and fine-grained management through this authorization user interface is currently in the development backlog for node-solid-server</i></p>
</div>
</body>
</html>
6 changes: 3 additions & 3 deletions lib/api/authn/webid-oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { routeResolvedFile } = require('../../utils')
const bodyParser = require('body-parser').urlencoded({ extended: false })
const OidcManager = require('../../models/oidc-manager')
const { LoginRequest } = require('../../requests/login-request')
const { ConsentRequest } = require('../../requests/consent-request')
const { SharingRequest } = require('../../requests/sharing-request')

const restrictToTopDomain = require('../../handlers/restrict-to-top-domain')

Expand Down Expand Up @@ -84,8 +84,8 @@ function middleware (oidc) {

router.post('/login/tls', bodyParser, LoginRequest.loginTls)

router.get('/consent', ConsentRequest.get)
router.post('/consent', bodyParser, ConsentRequest.giveConsent)
router.get('/sharing', SharingRequest.get)
router.post('/sharing', bodyParser, SharingRequest.share)

router.get('/account/password/reset', restrictToTopDomain, PasswordResetEmailRequest.get)
router.post('/account/password/reset', restrictToTopDomain, bodyParser, PasswordResetEmailRequest.post)
Expand Down
8 changes: 4 additions & 4 deletions lib/requests/auth-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,13 @@ class AuthRequest {
return url.format(signupUrl)
}

consentUrl () {
sharingUrl () {
let host = this.accountManager.host
let consentUrl = url.parse(url.resolve(host.serverUri, '/consent'))
let sharingUrl = url.parse(url.resolve(host.serverUri, '/sharing'))

consentUrl.query = this.authQueryParams
sharingUrl.query = this.authQueryParams

return url.format(consentUrl)
return url.format(sharingUrl)
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/requests/login-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ class LoginRequest extends AuthRequest {
postLoginUrl (validUser) {
// Login request is part of an app's auth flow
if (/token|code/.test(this.authQueryParams['response_type'])) {
return this.consentUrl()
return this.sharingUrl()
// Login request is a user going to /login in browser
} else if (validUser) {
return this.authQueryParams['redirect_uri'] || validUser.accountUri
Expand Down
84 changes: 55 additions & 29 deletions lib/requests/consent-request.js → lib/requests/sharing-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#')
/**
* Models a local Login request
*/
class ConsentRequest extends AuthRequest {
class SharingRequest extends AuthRequest {
/**
* @constructor
* @param options {Object}
Expand Down Expand Up @@ -48,7 +48,7 @@ class ConsentRequest extends AuthRequest {
static fromParams (req, res) {
let options = AuthRequest.requestOptions(req, res)

return new ConsentRequest(options)
return new SharingRequest(options)
}

/**
Expand All @@ -64,19 +64,23 @@ class ConsentRequest extends AuthRequest {
* @param res {ServerResponse}
*/
static async get (req, res) {
const request = ConsentRequest.fromParams(req, res)
const request = SharingRequest.fromParams(req, res)

const appOrigin = request.getAppOrigin()
// Check if is already registered or is data browser
const appUrl = request.getAppUrl()
const appOrigin = appUrl.origin
const serverUrl = new url.URL(req.app.locals.ldp.serverUri)

// Check if is already registered or is data browser or the webId is not on this machine
if (request.isUserLoggedIn()) {
if (
appOrigin === req.app.locals.ldp.serverUri ||
new url.URL(request.session.subject._id).origin !== serverUrl.origin ||
(appUrl && request.isSubdomain(serverUrl.host, appUrl.host) && appUrl.protocol === serverUrl.protocol) ||
await request.isAppRegistered(req.app.locals.ldp, appOrigin, request.session.subject._id)
) {
request.setUserConsent(appOrigin)
request.redirectPostConsent()
request.setUserShared(appOrigin)
request.redirectPostSharing()
} else {
request.renderForm(null, req)
request.renderForm(null, req, appOrigin)
}
}
}
Expand All @@ -90,31 +94,46 @@ class ConsentRequest extends AuthRequest {
*
* @return {Promise}
*/
static async giveConsent (req, res) {
static async share (req, res) {
let accessModes = []
let consented = false
if (req.body) {
accessModes = req.body.access_mode
accessModes = req.body.access_mode || []
if (!Array.isArray(accessModes)) {
accessModes = [ accessModes ]
}
consented = req.body.consent
}

let request = ConsentRequest.fromParams(req, res)
let request = SharingRequest.fromParams(req, res)

if (request.isUserLoggedIn()) {
const appOrigin = request.getAppOrigin()
debug('Providing consent for app sharing')
const appUrl = request.getAppUrl()
const appOrigin = `${appUrl.protocol}//${appUrl.host}`
debug('Sharing App')

if (consented) {
await request.registerApp(req.app.locals.ldp, appOrigin, accessModes, request.session.subject._id)
request.setUserConsent(appOrigin)
request.setUserShared(appOrigin)
}

// Redirect once that's all done
request.redirectPostConsent()
request.redirectPostSharing()
}
}

isSubdomain (domain, subdomain) {
const domainArr = domain.split('.')
const subdomainArr = subdomain.split('.')
for (let i = 1; i <= domainArr.length; i++) {
if (subdomainArr[subdomainArr.length - i] !== domainArr[domainArr.length - i]) {
return false
}
}
return true
}

setUserConsent (appOrigin) {
setUserShared (appOrigin) {
if (!this.session.consentedOrigins) {
this.session.consentedOrigins = []
}
Expand All @@ -133,16 +152,15 @@ class ConsentRequest extends AuthRequest {
return true
}

getAppOrigin () {
const parsed = url.parse(this.authQueryParams.redirect_uri)
return `${parsed.protocol}//${parsed.host}`
getAppUrl () {
return new url.URL(this.authQueryParams.redirect_uri)
}

async getProfileGraph (ldp, webId) {
return await new Promise(async (resolve, reject) => {
const store = $rdf.graph()
const profileText = await ldp.readResource(webId)
$rdf.parse(profileText.toString(), store, 'https://localhost:8443/profile/card', 'text/turtle', (error, kb) => {
$rdf.parse(profileText.toString(), store, this.getWebIdFile(webId), 'text/turtle', (error, kb) => {
if (error) {
reject(error)
} else {
Expand All @@ -153,10 +171,15 @@ class ConsentRequest extends AuthRequest {
}

async saveProfileGraph (ldp, store, webId) {
const text = $rdf.serialize(undefined, store, webId, 'text/turtle')
const text = $rdf.serialize(undefined, store, this.getWebIdFile(webId), 'text/turtle')
await ldp.put(webId, intoStream(text), 'text/turtle')
}

getWebIdFile (webId) {
const webIdurl = new url.URL(webId)
return `${webIdurl.origin}${webIdurl.path}`
}

async isAppRegistered (ldp, appOrigin, webId) {
const store = await this.getProfileGraph(ldp, webId)
return store.each($rdf.sym(webId), ACL('trustedApp')).find((app) => {
Expand All @@ -165,6 +188,7 @@ class ConsentRequest extends AuthRequest {
}

async registerApp (ldp, appOrigin, accessModes, webId) {
debug(`Registering app (${appOrigin}) with accessModes ${accessModes} for webId ${webId}`)
const store = await this.getProfileGraph(ldp, webId)
const origin = $rdf.sym(appOrigin)
// remove existing statements on same origin - if it exists
Expand All @@ -177,6 +201,7 @@ class ConsentRequest extends AuthRequest {
const application = new $rdf.BlankNode()
store.add($rdf.sym(webId), ACL('trustedApp'), application, webId)
store.add(application, ACL('origin'), origin, webId)

accessModes.forEach(mode => {
store.add(application, ACL('mode'), ACL(mode))
})
Expand All @@ -192,42 +217,43 @@ class ConsentRequest extends AuthRequest {
*
* @return {string}
*/
postConsentUrl () {
postSharingUrl () {
return this.authorizeUrl()
}

/**
* Redirects the Login request to continue on the OIDC auth workflow.
*/
redirectPostConsent () {
let uri = this.postConsentUrl()
redirectPostSharing () {
let uri = this.postSharingUrl()
debug('Login successful, redirecting to ', uri)
this.response.redirect(uri)
}

/**
* Renders the login form
*/
renderForm (error, req) {
renderForm (error, req, appOrigin) {
let queryString = req && req.url && req.url.replace(/[^?]+\?/, '') || ''
let params = Object.assign({}, this.authQueryParams,
{
registerUrl: this.registerUrl(),
returnToUrl: this.returnToUrl,
enablePassword: this.localAuth.password,
enableTls: this.localAuth.tls,
tlsUrl: `/login/tls?${encodeURIComponent(queryString)}`
tlsUrl: `/login/tls?${encodeURIComponent(queryString)}`,
app_origin: appOrigin
})

if (error) {
params.error = error.message
this.response.status(error.statusCode)
}

this.response.render('auth/consent', params)
this.response.render('auth/sharing', params)
}
}

module.exports = {
ConsentRequest
SharingRequest
}
Loading

0 comments on commit 4d36e12

Please sign in to comment.