From 95646fa3e9cf2d0940c81d9704939c16d6316699 Mon Sep 17 00:00:00 2001 From: "jan.michael.yu+github@gmail.com" Date: Sun, 1 May 2016 22:42:18 +0800 Subject: [PATCH 1/2] WIP adding profile security to user information document --- app/templates/node-server/proxy.js | 10 +- app/templates/node-server/routes.js | 99 ++++++++++++++----- app/templates/rest-api/ext/profile.xqy | 88 +++++++++++++++++ app/templates/src/lib/user-model.xqy | 71 +++++++++++++ .../ui/app/user/profile.controller.js | 25 ++--- app/templates/ui/app/user/profile.html | 5 + slushfile.js | 4 + 7 files changed, 263 insertions(+), 39 deletions(-) create mode 100644 app/templates/rest-api/ext/profile.xqy create mode 100644 app/templates/src/lib/user-model.xqy diff --git a/app/templates/node-server/proxy.js b/app/templates/node-server/proxy.js index c3d9c406..c7fef231 100644 --- a/app/templates/node-server/proxy.js +++ b/app/templates/node-server/proxy.js @@ -33,11 +33,11 @@ router.put('*', function(req, res) { // For PUT requests, require authentication if (req.session.user === undefined) { res.status(401).send('Unauthorized'); - } else if (req.path === '/v1/documents' && - req.query.uri.match('/api/users/') && - req.query.uri.match(new RegExp('/api/users/[^(' + req.session.user.name + ')]+.json'))) { - // The user is try to PUT to a profile document other than his/her own. Not allowed. - res.status(403).send('Forbidden'); + // } else if (req.path === '/v1/documents' && + // req.query.uri.match('/api/users/') && + // req.query.uri.match(new RegExp('/api/users/[^(' + req.session.user.name + ')]+.json'))) { + // // The user is try to PUT to a profile document other than his/her own. Not allowed. + // res.status(403).send('Forbidden'); } else { if (req.path === '/v1/documents' && req.query.uri.match('/users/')) { // TODO: The user is updating the profile. Update the session info. diff --git a/app/templates/node-server/routes.js b/app/templates/node-server/routes.js index f551e36c..8f107fc6 100644 --- a/app/templates/node-server/routes.js +++ b/app/templates/node-server/routes.js @@ -17,47 +17,78 @@ var options = { }; // [GJo] (#31) Moved bodyParsing inside routing, otherwise it might try to parse uploaded binaries as json.. -router.use(bodyParser.urlencoded({extended: true})); +router.use(bodyParser.urlencoded({ extended: true })); router.use(bodyParser.json()); router.get('/user/status', function(req, res) { var headers = req.headers; noCache(res); + if (req.session.user === undefined) { - res.send({authenticated: false}); + res.send({ authenticated: false }); } else { delete headers['content-length']; + + var username = req.session.user.name; + var password = req.session.user.password; + var status = http.get({ hostname: options.mlHost, port: options.mlHttpPort, - path: '/v1/documents?uri=/api/users/' + req.session.user.name + '.json', + path: '/v1/documents?uri=/users/' + username + '.json', headers: headers, - auth: req.session.user.name + ':' + req.session.user.password + auth: username + ':' + password }, function(response) { + console.log('login response : ' + response); + if (response.statusCode === 401) { + res.statusCode = 401; + res.send('Unauthenticated'); + } else if (response.statusCode === 404) { + // authentication successful, but no profile defined + req.session.user = { + name: username, + password: password + }; + res.status(200).send({ + authenticated: true, + username: username + }); + } else { + console.log('code: ' + response.statusCode); if (response.statusCode === 200) { + // authentication successful, remember the username + req.session.user = { + name: username, + password: password + }; response.on('data', function(chunk) { + console.log('chunk: ' + chunk); + var json = JSON.parse(chunk); + req.session.user.profile = {}; + + if (json.user !== undefined) { + req.session.user.profile.fullname = json.user.fullname; + req.session.user.profile.emails = json.user.emails; + } + req.session.user.profile.webroles = json.webroles; + if (json.user !== undefined) { res.status(200).send({ authenticated: true, - username: req.session.user.name, + username: username, profile: json.user }); } else { console.log('did not find chunk.user'); } }); - } else if (response.statusCode === 404) { - //no profile yet for user - res.status(200).send({ - authenticated: true, - username: req.session.user.name, - profile: {} - }); } else { - res.send({authenticated: false}); + res.statusCode = response.statusCode; + res.send(response.statusMessage); } - }); + } + }); status.on('error', function(e) { console.log(JSON.stringify(e)); @@ -79,13 +110,16 @@ router.post('/user/login', function(req, res) { // remove content length so ML doesn't wait for request body // that isn't being passed. delete headers['content-length']; - var login = http.get({ + var login = http.request({ + method: 'POST', hostname: options.mlHost, port: options.mlHttpPort, - path: '/v1/documents?uri=/api/users/' + username + '.json', - headers: headers, + // path: '/v1/documents?uri=/api/users/' + username + '.json', + path: '/v1/resources/profile', + // headers: headers, auth: username + ':' + password }, function(response) { + console.log('login response : ' + response); if (response.statusCode === 401) { res.statusCode = 401; res.send('Unauthenticated'); @@ -108,7 +142,17 @@ router.post('/user/login', function(req, res) { password: password }; response.on('data', function(chunk) { + console.log('chunk: ' + chunk); + var json = JSON.parse(chunk); + req.session.user.profile = {}; + + if (json.user !== undefined) { + req.session.user.profile.fullname = json.user.fullname; + req.session.user.profile.emails = json.user.emails; + } + req.session.user.profile.webroles = json.webroles; + if (json.user !== undefined) { res.status(200).send({ authenticated: true, @@ -126,9 +170,20 @@ router.post('/user/login', function(req, res) { } }); + login.end(); + + login.on('socket', function(socket) { + socket.setTimeout(10000); + socket.on('timeout', function() { + console.log('timeout..'); + login.abort(); + }); + }); + login.on('error', function(e) { - console.log(JSON.stringify(e)); - console.log('login failed: ' + e.statusCode); + console.log('login failed: ' + e); + login.abort(); + res.status(500).send('Login failed'); }); }); @@ -140,9 +195,9 @@ router.get('/user/logout', function(req, res) { router.get('/*', four0four.notFoundMiddleware); -function noCache(response){ - response.append('Cache-Control', 'no-cache, must-revalidate');//HTTP 1.1 - must-revalidate - response.append('Pragma', 'no-cache');//HTTP 1.0 +function noCache(response) { + response.append('Cache-Control', 'no-cache, must-revalidate'); //HTTP 1.1 - must-revalidate + response.append('Pragma', 'no-cache'); //HTTP 1.0 response.append('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT'); // Date in the past } diff --git a/app/templates/rest-api/ext/profile.xqy b/app/templates/rest-api/ext/profile.xqy new file mode 100644 index 00000000..c08fe9d8 --- /dev/null +++ b/app/templates/rest-api/ext/profile.xqy @@ -0,0 +1,88 @@ +xquery version "1.0-ml"; + +module namespace profile = "http://marklogic.com/rest-api/resource/profile"; + +import module namespace json="http://marklogic.com/xdmp/json" + at "/MarkLogic/json/json.xqy"; +import module namespace user = "http://marklogic.com/slush/user-model" + at "/lib/user-model.xqy"; + +declare namespace roxy = "http://marklogic.com/roxy"; + +declare option xdmp:mapping "false"; + +declare variable $ROLE_READER := "marklogic-slush-reader-role"; +declare variable $ROLE_WRITER := "marklogic-slush-writer-role"; +declare variable $ROLE_ADMIN := "marklogic-slush-admin-role"; + +(: + + This gets the profile if it exists, + and the user's webroles are determined based on the user's system roles for this app + + :) +declare function profile:post( + $context as map:map, + $params as map:map, + $input as document-node()* +) as document-node()* +{ + map:put($context, "output-types", "application/json"), + let $username := xdmp:get-current-user() + let $uri := "/users/"||$username||".json" + let $profile := fn:doc($uri) + let $profile := + if ($profile/element()) + then json:transform-to-json-object($profile) + else if ($profile) + then xdmp:from-json($profile) + else ( + let $object := json:object() + let $_ := map:put($object, "user", json:object()) + return $object + ) + + let $webroles := profile:get-webroles-for-user() + let $webroles-array := json:array() + let $_ := + for $webrole in $webroles + return json:array-push($webroles-array, $webrole) + + let $_ := map:put($profile, "webroles", $webroles-array) + + let $user := map:get($profile, "user") + + return document{ xdmp:to-json($profile) } +}; + +(: + Rather than expose the system role names, we'll represent them as "webroles" that the UI can use for permission checking. +:) +declare private function profile:get-webroles-for-user() { +(: if (xdmp:get-current-roles() = (xdmp:role("admin"), xdmp:role($ROLE_ADMIN))) + then ("admin", "writer", "reader") + else if (xdmp:get-current-roles() = xdmp:role($ROLE_WRITER)) + then ("writer", "reader") + else if (xdmp:get-current-roles() = xdmp:role($ROLE_READER)) + then "reader" + else () + :) + + if (xdmp:get-current-roles() = (xdmp:role("admin"))) + then ("admin", "writer", "reader") + else () +}; + +declare function profile:put( + $context as map:map, + $params as map:map, + $input as document-node()* +) as document-node()? +{ + map:put($context, "output-types", "application/json"), + let $username := xdmp:get-current-user() + let $profile := + user:convert($input) + let $_ := user:put($username, $profile) + return document{ '"ok"' } +}; diff --git a/app/templates/src/lib/user-model.xqy b/app/templates/src/lib/user-model.xqy new file mode 100644 index 00000000..3db0722e --- /dev/null +++ b/app/templates/src/lib/user-model.xqy @@ -0,0 +1,71 @@ +xquery version "1.0-ml"; + +module namespace user = "http://marklogic.com/slush/user-model"; + +import module namespace json = "http://marklogic.com/xdmp/json" at "/MarkLogic/json/json.xqy"; + +declare namespace alert = "http://marklogic.com/xdmp/alert"; +declare namespace jbasic = "http://marklogic.com/xdmp/json/basic"; + +declare option xdmp:mapping "false"; + +declare variable $is-ml8 := fn:starts-with(xdmp:version(), "8"); + +(: access :) + +declare function user:replace($profile as element(jbasic:json), $properties as element()*, $new-properties as element()*) as element(jbasic:json) { + element { fn:node-name($profile) } { + $profile/@*, + $profile/(* except $profile/jbasic:user), + element jbasic:user { + attribute type { "object"}, + $profile/jbasic:user/(@* except @type), + + $profile/jbasic:user/(* except $properties), + $new-properties + } + } +}; + +(: low-level access :) + +declare function user:uri($id as xs:string) as xs:string { + fn:concat('/users/', $id, '.json') +}; + +declare function user:get($id as xs:string) as element(jbasic:json) { + let $uri := user:uri($id) + return + user:read($uri) +}; + +declare function user:put($id as xs:string, $profile as element(jbasic:json)) { + let $uri := user:uri($id) + return + user:save($uri, $profile) +}; + +declare function user:convert($doc as document-node()?) as element(jbasic:json) { + if ($doc/jbasic:json) then + $doc/jbasic:json + else if ($doc) then + json:transform-from-json($doc) + else + +}; + +declare function user:read($uri as xs:string) as element(jbasic:json) { + let $doc := fn:doc($uri) + return + user:convert($doc) +}; + +declare function user:save($uri as xs:string, $profile as element(jbasic:json)) { + let $profile := + if ($is-ml8) then + xdmp:to-json(json:transform-to-json($profile))/node() + else + $profile + return + xdmp:document-insert($uri, $profile, (xdmp:default-permissions(), xdmp:document-get-permissions($uri)), (xdmp:default-collections(), 'users', xdmp:document-get-collections($uri))) +}; diff --git a/app/templates/ui/app/user/profile.controller.js b/app/templates/ui/app/user/profile.controller.js index 016b4f74..1483f466 100644 --- a/app/templates/ui/app/user/profile.controller.js +++ b/app/templates/ui/app/user/profile.controller.js @@ -48,19 +48,20 @@ _.pull(ctrl.user.emails, ''); } - mlRest.updateDocument({ - user: { - 'fullname': ctrl.user.fullname, - 'emails': ctrl.user.emails + ctrl.message = undefined; + mlRest.callExtension('profile', { + method: 'PUT', + data: { + user: { + fullname: ctrl.user.fullname, + emails: ctrl.user.emails + } + }, + headers: { + 'Content-Type': 'application/json' } - }, { - format: 'json', - uri: '/api/users/' + ctrl.user.name + '.json' - // TODO: add read/update permissions here like this: - // 'perm:sample-role': 'read', - // 'perm:sample-role': 'update' - }).then(function(data) { - $state.go('root'); + }).then(function(){ + ctrl.message = 'Profile stored successfully'; }); } } diff --git a/app/templates/ui/app/user/profile.html b/app/templates/ui/app/user/profile.html index 5df42dbe..95c07601 100644 --- a/app/templates/ui/app/user/profile.html +++ b/app/templates/ui/app/user/profile.html @@ -45,5 +45,10 @@

Edit your profile

Cancel +
+

+ {{ctrl.message}} +

+
diff --git a/slushfile.js b/slushfile.js index 9113ca97..dcbdf138 100644 --- a/slushfile.js +++ b/slushfile.js @@ -240,7 +240,11 @@ function configRoxy() { ' false\n' + ' \n'); + // TODO: add amps for profile and user-model + fs.writeFileSync('deploy/ml-config.xml', foo); + + } catch (e) { console.log('failed to update configuration: ' + e.message); } From a0bc1021712c18cfd1dc7425b9955eabe9c072f4 Mon Sep 17 00:00:00 2001 From: "jan.michael.yu+github@gmail.com" Date: Mon, 2 May 2016 14:36:37 +0800 Subject: [PATCH 2/2] adding roles for default user for ML security --- app/templates/node-server/routes.js | 1 - slushfile.js | 101 +++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/app/templates/node-server/routes.js b/app/templates/node-server/routes.js index 8f107fc6..1e79dc91 100644 --- a/app/templates/node-server/routes.js +++ b/app/templates/node-server/routes.js @@ -39,7 +39,6 @@ router.get('/user/status', function(req, res) { headers: headers, auth: username + ':' + password }, function(response) { - console.log('login response : ' + response); if (response.statusCode === 401) { res.statusCode = 401; res.send('Unauthenticated'); diff --git a/slushfile.js b/slushfile.js index dcbdf138..a2e212c2 100644 --- a/slushfile.js +++ b/slushfile.js @@ -240,7 +240,106 @@ function configRoxy() { ' false\n' + ' \n'); - // TODO: add amps for profile and user-model + foo = foo.replace(/^\s*xdmp:with-namespaces<\/privilege-name>/m, + ' xdmp:with-namespaces\n' + + ' \n' + + ' \n' + + ' rest-reader\n' + + ' \n' + + ' \n' + + ' rest-writer' + + ' \n' + + ' \n' + + ' \n' + + '\n' + + ' \n' + + ' @ml.app-name-amp-role\n' + + ' A role for amping function of the @ml.app-name application\n' + + ' \n' + + ' \n' + + ' any-collection\n' + + ' \n' + + ' \n' + + ' xdmp:email\n' + + ' \n' + + ' \n' + + ' users-uri' + ); + + foo = foo.replace(/^\s*(\r|\n)^\s*<\/role-names>/m, + ' \n' + + ' rest-extension-user\n' + + ' ' + ); + + foo = foo.replace(/^\s*<\/amp>(\r|\n)^\s*-->/m, + ' \n' + + '-->\n' + + ' \n' + + ' http://marklogic.com/slush/user-model\n' + + ' save\n' + + ' /lib/user-model.xqy\n' + + ' @ml.app-modules-db\n' + + ' @ml.app-name-amp-role\n' + + ' \n' + + ' \n' + + ' http://marklogic.com/rest-api/resource/profile\n' + + ' put\n' + + ' /marklogic.rest.resource/profile/assets/resource.xqy\n' + + ' @ml.app-modules-db\n' + + ' @ml.app-name-update-role\n' + + ' ' + ); + + foo = foo.replace(/^\s*uri<\/kind>(\r|\n)^\s*<\/privilege>(\r|\n)^\s*-->/m, + ' uri\n' + + ' \n' + + '-->\n' + + ' \n' + + ' users-uri\n' + + ' /users/\n' + + ' uri\n' + + ' \n' + ); + + foo = foo.replace(/^\s*/m, + ' \n' + + ' \n' + + ' \n' + + ' @ml.app-name-read-role\n' + + ' A low level read role for documents and modules of the @ml.app-name application\n' + + ' \n' + + ' \n' + + ' @ml.app-name-insert-role\n' + + ' A low level insert role for documents of the @ml.app-name application\n' + + ' \n' + + ' \n' + + ' @ml.app-name-update-role\n' + + ' A low level update role for documents of the @ml.app-name application\n' + + ' \n' + + ' \n' + + ' @ml.app-name-execute-role\n' + + ' A low level execute role for modules of the @ml.app-name application\n' + + ' \n' + + ' \n' + + ' @ml.app-name-defaults-role\n' + + ' A low level role providing default permissions for documents of the @ml.app-name application\n' + + ' \n' + + ' \n' + + ' read\n' + + ' @ml.app-name-read-role\n' + + ' \n' + + ' \n' + + ' insert\n' + + ' @ml.app-name-insert-role\n' + + ' \n' + + ' \n' + + ' update\n' + + ' @ml.app-name-update-role\n' + + ' \n' + + ' \n' + + ' ' + ); fs.writeFileSync('deploy/ml-config.xml', foo);