Commit 7c535c67 authored by Nick Stenning's avatar Nick Stenning

Merge pull request #2687 from...

Merge pull request #2687 from hypothesis/github-2686-fix-invalid-permissions-when-creating-annotations-when-signed-out

Set permissions of new annotations on login
parents 887ac52d d53580a5
### global -validate ### ### global -validate ###
STORAGE_KEY = 'hypothesis.privacy' events = require('../events')
# Validate an annotation. # Validate an annotation.
# Annotations must be attributed to a user or marked as deleted. # Annotations must be attributed to a user or marked as deleted.
...@@ -46,10 +46,10 @@ errorMessage = (reason) -> ...@@ -46,10 +46,10 @@ errorMessage = (reason) ->
AnnotationController = [ AnnotationController = [
'$scope', '$timeout', '$q', '$rootScope', '$document', '$scope', '$timeout', '$q', '$rootScope', '$document',
'drafts', 'flash', 'permissions', 'tags', 'time', 'drafts', 'flash', 'permissions', 'tags', 'time',
'annotationUI', 'annotationMapper', 'session', 'groups', 'localStorage' 'annotationUI', 'annotationMapper', 'session', 'groups',
($scope, $timeout, $q, $rootScope, $document, ($scope, $timeout, $q, $rootScope, $document,
drafts, flash, permissions, tags, time, drafts, flash, permissions, tags, time,
annotationUI, annotationMapper, session, groups, localStorage) -> annotationUI, annotationMapper, session, groups) ->
@annotation = {} @annotation = {}
@action = 'view' @action = 'view'
...@@ -61,19 +61,14 @@ AnnotationController = [ ...@@ -61,19 +61,14 @@ AnnotationController = [
model = $scope.annotationGet() model = $scope.annotationGet()
model.user ?= session.state.userid
# Set the group of new annotations. # Set the group of new annotations.
if not model.group if not model.group
model.group = groups.focused().id model.group = groups.focused().id
# Set the permissions of new annotations. # Set the permissions of new annotations.
if not model.permissions model.permissions = model.permissions or permissions.default(model.group)
defaultLevel = localStorage.getItem(STORAGE_KEY);
if (defaultLevel != 'private') and (defaultLevel != 'shared')
defaultLevel = 'shared';
if defaultLevel == 'private'
model.permissions = permissions.private()
else
model.permissions = permissions.shared(model.group)
highlight = model.$highlight highlight = model.$highlight
original = null original = null
...@@ -140,7 +135,7 @@ AnnotationController = [ ...@@ -140,7 +135,7 @@ AnnotationController = [
# next time they create an annotation. # next time they create an annotation.
# But _don't_ cache it when they change the privacy level of a reply. # But _don't_ cache it when they change the privacy level of a reply.
if not model.references # If the annotation is not a reply. if not model.references # If the annotation is not a reply.
localStorage.setItem(STORAGE_KEY, privacy); permissions.setDefault(privacy)
if privacy == 'private' if privacy == 'private'
@annotation.permissions = permissions.private() @annotation.permissions = permissions.private()
...@@ -383,11 +378,15 @@ AnnotationController = [ ...@@ -383,11 +378,15 @@ AnnotationController = [
this.render() this.render()
, true , true
# Watch the current user $scope.$on(events.USER_CHANGED, ->
# TODO: fire events instead since watchers are not free and auth is rare model.user ?= session.state.userid
$scope.$watch (-> session.state.userid), (userid) ->
model.permissions ?= {} # Set model.permissions on sign in, if it isn't already set.
model.user ?= userid # This is because you can create annotations when signed out and they
# will have model.permissions = null, then when you sign in we set the
# permissions correctly here.
model.permissions = model.permissions or permissions.default(model.group)
)
# Start editing brand new annotations immediately # Start editing brand new annotations immediately
unless model.id? or (this.isHighlight() and highlight) then this.edit() unless model.id? or (this.isHighlight() and highlight) then this.edit()
......
{module, inject} = angular.mock {module, inject} = angular.mock
events = require('../../events')
describe 'annotation', -> describe 'annotation', ->
$compile = null $compile = null
$document = null $document = null
...@@ -27,7 +29,6 @@ describe 'annotation', -> ...@@ -27,7 +29,6 @@ describe 'annotation', ->
fakeTags = null fakeTags = null
fakeTime = null fakeTime = null
fakeUrlEncodeFilter = null fakeUrlEncodeFilter = null
fakeLocalStorage = null
sandbox = null sandbox = null
createDirective = -> createDirective = ->
...@@ -71,6 +72,8 @@ describe 'annotation', -> ...@@ -71,6 +72,8 @@ describe 'annotation', ->
permits: sandbox.stub().returns(true) permits: sandbox.stub().returns(true)
shared: sandbox.stub().returns({read: ['everybody']}) shared: sandbox.stub().returns({read: ['everybody']})
private: sandbox.stub().returns({read: ['justme']}) private: sandbox.stub().returns({read: ['justme']})
default: sandbox.stub().returns({read: ['default']})
setDefault: sandbox.stub()
} }
fakePersonaFilter = sandbox.stub().returnsArg(0) fakePersonaFilter = sandbox.stub().returnsArg(0)
fakeDocumentTitleFilter = (arg) -> '' fakeDocumentTitleFilter = (arg) -> ''
...@@ -89,10 +92,6 @@ describe 'annotation', -> ...@@ -89,10 +92,6 @@ describe 'annotation', ->
nextFuzzyUpdate: sandbox.stub().returns(30) nextFuzzyUpdate: sandbox.stub().returns(30)
} }
fakeUrlEncodeFilter = (v) -> encodeURIComponent(v) fakeUrlEncodeFilter = (v) -> encodeURIComponent(v)
fakeLocalStorage = {
setItem: sandbox.stub()
getItem: sandbox.stub()
}
fakeGroups = { fakeGroups = {
focused: -> {} focused: -> {}
...@@ -114,7 +113,6 @@ describe 'annotation', -> ...@@ -114,7 +113,6 @@ describe 'annotation', ->
$provide.value 'tags', fakeTags $provide.value 'tags', fakeTags
$provide.value 'time', fakeTime $provide.value 'time', fakeTime
$provide.value 'urlencodeFilter', fakeUrlEncodeFilter $provide.value 'urlencodeFilter', fakeUrlEncodeFilter
$provide.value 'localStorage', fakeLocalStorage
$provide.value 'groups', fakeGroups $provide.value 'groups', fakeGroups
return return
...@@ -152,6 +150,7 @@ describe 'annotation', -> ...@@ -152,6 +150,7 @@ describe 'annotation', ->
$scope.$digest() $scope.$digest()
assert.notCalled annotation.$create assert.notCalled annotation.$create
fakeSession.state.userid = 'acct:ted@wyldstallyns.com' fakeSession.state.userid = 'acct:ted@wyldstallyns.com'
$scope.$broadcast(events.USER_CHANGED, {})
$scope.$digest() $scope.$digest()
assert.calledOnce annotation.$create assert.calledOnce annotation.$create
...@@ -240,8 +239,7 @@ describe 'annotation', -> ...@@ -240,8 +239,7 @@ describe 'annotation', ->
controller.edit(); controller.edit();
controller.setPrivacy('shared') controller.setPrivacy('shared')
controller.save().then(-> controller.save().then(->
assert(fakeLocalStorage.setItem.calledWithExactly( assert(fakePermissions.setDefault.calledWithExactly('shared'))
'hypothesis.privacy', 'shared'))
) )
it 'saves the "private" visibility level to localStorage', -> it 'saves the "private" visibility level to localStorage', ->
...@@ -249,8 +247,7 @@ describe 'annotation', -> ...@@ -249,8 +247,7 @@ describe 'annotation', ->
controller.edit(); controller.edit();
controller.setPrivacy('private') controller.setPrivacy('private')
controller.save().then(-> controller.save().then(->
assert(fakeLocalStorage.setItem.calledWithExactly( assert(fakePermissions.setDefault.calledWithExactly('private'))
'hypothesis.privacy', 'private'))
) )
it "doesn't save the visibility if the annotation is a reply", -> it "doesn't save the visibility if the annotation is a reply", ->
...@@ -259,7 +256,7 @@ describe 'annotation', -> ...@@ -259,7 +256,7 @@ describe 'annotation', ->
controller.edit(); controller.edit();
controller.setPrivacy('private') controller.setPrivacy('private')
controller.save().then(-> controller.save().then(->
assert(not fakeLocalStorage.setItem.called) assert(not fakePermissions.setDefault.called)
) )
describe '#hasContent', -> describe '#hasContent', ->
...@@ -587,8 +584,10 @@ describe("AnnotationController", -> ...@@ -587,8 +584,10 @@ describe("AnnotationController", ->
isPrivate: (permissions, user) -> isPrivate: (permissions, user) ->
return user in permissions.read return user in permissions.read
permits: -> true permits: -> true
shared: (group) -> {"read": [group]} shared: -> {}
private: -> {"read": [session.state.userid]} private: -> {}
default: -> {}
setDefault: ->
} }
session: session session: session
tags: tags or {store: ->} tags: tags or {store: ->}
...@@ -647,44 +646,57 @@ describe("AnnotationController", -> ...@@ -647,44 +646,57 @@ describe("AnnotationController", ->
it("creates the directive without crashing", -> it("creates the directive without crashing", ->
createAnnotationDirective({}) createAnnotationDirective({})
) )
)
it("sets the permissions of new annotations from local storage", -> it("sets the user of new annotations", ->
{controller} = createAnnotationDirective({ annotation = {}
localStorage: {
setItem: ->
getItem: (key) ->
if key == 'hypothesis.privacy'
return 'shared'
else
assert(false, "Wrong key requested from localStorage")
}
})
assert(controller.isShared())
)
it("sets the permissions of new annotations from local storage to private", -> {session} = createAnnotationDirective({annotation: annotation})
{controller} = createAnnotationDirective({
localStorage: {
setItem: ->
getItem: (key) ->
if key == 'hypothesis.privacy'
return 'private'
else
assert(false, "Wrong key requested from localStorage")
}
})
assert(controller.isPrivate())
)
it("defaults to shared if no locally cached visibility", -> assert.equal(annotation.user, session.state.userid)
{controller} = createAnnotationDirective({ )
localStorage: {
setItem: -> it("sets the permissions of new annotations", ->
getItem: -> # This is a new annotation, doesn't have any permissions yet.
} annotation = {group: "test-group"}
}) permissions = {
assert(controller.isShared()) default: sinon.stub().returns("default permissions")
) isShared: ->
isPrivate: ->
}
createAnnotationDirective({
annotation: annotation, permissions: permissions})
assert(permissions.default.calledWithExactly("test-group"))
assert.equal(annotation.permissions, "default permissions",
"It should set a new annotation's permissions to what
permissions.default() returns")
)
it("doesn't overwrite permissions if the annotation already has them", ->
# The annotation will already have some permissions, before it's
# passed to AnnotationController.
annotation = {
permissions: {
read: ["foo"]
update: ["bar"]
delete: ["gar"]
admin: ["har"]
}
}
# Save the original permissions for asserting against later.
original_permissions = JSON.parse(JSON.stringify(annotation.permissions))
permissions = {
default: sinon.stub().returns("new permissions")
isShared: ->
isPrivate: ->
}
createAnnotationDirective(
{annotation: annotation, permissions: permissions})
assert.deepEqual(annotation.permissions, original_permissions)
) )
describe("save", -> describe("save", ->
...@@ -718,6 +730,66 @@ describe("AnnotationController", -> ...@@ -718,6 +730,66 @@ describe("AnnotationController", ->
) )
) )
describe("when the user signs in", ->
it("sets the user of unsaved annotations", ->
# This annotation has no user yet, because that's what happens
# when you create a new annotation while not signed in.
annotation = {}
session = {state: {userid: null}} # Not signed in.
{$rootScope} = createAnnotationDirective(
{annotation: annotation, session:session})
# At this point we would not expect the user to have been set,
# even though the annotation has been created, because the user isn't
# signed in.
assert(!annotation.user)
# Sign the user in.
session.state.userid = "acct:fred@hypothes.is"
# The session service would broadcast USER_CHANGED after sign in.
$rootScope.$broadcast(events.USER_CHANGED, {})
assert.equal(annotation.user, session.state.userid)
)
it("sets the permissions of unsaved annotations", ->
# This annotation has no permissions yet, because that's what happens
# when you create a new annotation while not signed in.
annotation = {group: "__world__"}
session = {state: {userid: null}} # Not signed in.
permissions = {
# permissions.default() would return null, because the user isn't
# signed in.
default: -> null
# We just need to mock these two functions so that it doesn't crash.
isShared: ->
isPrivate: ->
}
{$rootScope} = createAnnotationDirective(
{annotation: annotation, session:session, permissions: permissions})
# At this point we would not expect the permissions to have been set,
# even though the annotation has been created, because the user isn't
# signed in.
assert(!annotation.permissions)
# Sign the user in.
session.state.userid = "acct:fred@hypothes.is"
# permissions.default() would now return permissions, because the user
# is signed in.
permissions.default = -> "__default_permissions__"
# The session service would broadcast USER_CHANGED after sign in.
$rootScope.$broadcast(events.USER_CHANGED, {})
assert.equal(annotation.permissions, "__default_permissions__")
)
)
### ###
Simulate what happens when the user edits an annotation, clicks Save, Simulate what happens when the user edits an annotation, clicks Save,
gets an error because the server fails to save the annotation, then clicks gets an error because the server fails to save the annotation, then clicks
......
STORAGE_KEY = 'hypothesis.privacy'
###* ###*
# @ngdoc service # @ngdoc service
# @name Permissions # @name Permissions
...@@ -6,7 +8,7 @@ ...@@ -6,7 +8,7 @@
# This service can set default permissions to annotations properly and # This service can set default permissions to annotations properly and
# offers some utility functions regarding those. # offers some utility functions regarding those.
### ###
module.exports = ['session', (session) -> module.exports = ['session', 'localStorage', (session, localStorage) ->
ALL_PERMISSIONS = {} ALL_PERMISSIONS = {}
GROUP_WORLD = 'group:__world__' GROUP_WORLD = 'group:__world__'
ADMIN_PARTY = [{ ADMIN_PARTY = [{
...@@ -37,10 +39,15 @@ module.exports = ['session', (session) -> ...@@ -37,10 +39,15 @@ module.exports = ['session', (session) ->
# Typical use: annotation.permissions = permissions.private() # Typical use: annotation.permissions = permissions.private()
### ###
private: -> private: ->
read: [session.state.userid] if not session.state.userid
update: [session.state.userid] return null
delete: [session.state.userid] else
admin: [session.state.userid] return {
read: [session.state.userid]
update: [session.state.userid]
delete: [session.state.userid]
admin: [session.state.userid]
}
###* ###*
# @ngdoc method # @ngdoc method
...@@ -52,6 +59,8 @@ module.exports = ['session', (session) -> ...@@ -52,6 +59,8 @@ module.exports = ['session', (session) ->
# Typical use: annotation.permissions = permissions.shared(group) # Typical use: annotation.permissions = permissions.shared(group)
### ###
shared: (group) -> shared: (group) ->
if not session.state.userid
return null
if group? if group?
group = 'group:' + group group = 'group:' + group
else else
...@@ -63,6 +72,25 @@ module.exports = ['session', (session) -> ...@@ -63,6 +72,25 @@ module.exports = ['session', (session) ->
admin: [session.state.userid] admin: [session.state.userid]
} }
# Return the initial permissions for a new annotation in the given group.
default: (group) ->
defaultLevel = localStorage.getItem(STORAGE_KEY);
if (defaultLevel != 'private') and (defaultLevel != 'shared')
defaultLevel = 'shared';
if defaultLevel == 'private'
return this.private()
else
return this.shared(group)
# Set the default initial permissions for new annotations (that will be
# returned by default() above) to "private" or "shared" permissions.
#
# @param {string} private_or_shared "private" to make default() return the
# necessary permissions for private annotations, "shared" to make it
# return those for shared annotations.
setDefault: (private_or_shared) ->
localStorage.setItem(STORAGE_KEY, private_or_shared);
###* ###*
# @ngdoc method # @ngdoc method
# @name permissions#isShared # @name permissions#isShared
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
describe 'h:permissions', -> describe 'h:permissions', ->
sandbox = null sandbox = null
fakeSession = null fakeSession = null
fakeLocalStorage = null
before -> before ->
angular.module('h', []) angular.module('h', [])
...@@ -17,8 +18,12 @@ describe 'h:permissions', -> ...@@ -17,8 +18,12 @@ describe 'h:permissions', ->
userid: 'acct:flash@gordon' userid: 'acct:flash@gordon'
} }
} }
fakeLocalStorage = {
getItem: -> undefined
}
$provide.value 'session', fakeSession $provide.value 'session', fakeSession
$provide.value 'localStorage', fakeLocalStorage
return return
afterEach -> afterEach ->
...@@ -31,26 +36,70 @@ describe 'h:permissions', -> ...@@ -31,26 +36,70 @@ describe 'h:permissions', ->
beforeEach inject (_permissions_) -> beforeEach inject (_permissions_) ->
permissions = _permissions_ permissions = _permissions_
it 'private call fills all permissions with auth.user', -> describe 'private()', ->
perms = permissions.private()
assert.equal(perms.read[0], 'acct:flash@gordon') it 'fills all permissions with auth.user', ->
assert.equal(perms.update[0], 'acct:flash@gordon') perms = permissions.private()
assert.equal(perms.delete[0], 'acct:flash@gordon') assert.equal(perms.read[0], 'acct:flash@gordon')
assert.equal(perms.admin[0], 'acct:flash@gordon') assert.equal(perms.update[0], 'acct:flash@gordon')
assert.equal(perms.delete[0], 'acct:flash@gordon')
it 'public call fills the read property with group:__world__', -> assert.equal(perms.admin[0], 'acct:flash@gordon')
perms = permissions.shared()
assert.equal(perms.read[0], 'group:__world__') it 'returns null if session.state.userid is falsey', ->
assert.equal(perms.update[0], 'acct:flash@gordon') delete fakeSession.state.userid
assert.equal(perms.delete[0], 'acct:flash@gordon') assert.equal(permissions.private(), null)
assert.equal(perms.admin[0], 'acct:flash@gordon')
describe 'shared()', ->
it 'public call fills the read property with group:foo if passed "foo"', ->
perms = permissions.shared("foo") it 'fills the read property with group:__world__', ->
assert.equal(perms.read[0], 'group:foo') perms = permissions.shared()
assert.equal(perms.update[0], 'acct:flash@gordon') assert.equal(perms.read[0], 'group:__world__')
assert.equal(perms.delete[0], 'acct:flash@gordon') assert.equal(perms.update[0], 'acct:flash@gordon')
assert.equal(perms.admin[0], 'acct:flash@gordon') assert.equal(perms.delete[0], 'acct:flash@gordon')
assert.equal(perms.admin[0], 'acct:flash@gordon')
it 'fills the read property with group:foo if passed "foo"', ->
perms = permissions.shared("foo")
assert.equal(perms.read[0], 'group:foo')
assert.equal(perms.update[0], 'acct:flash@gordon')
assert.equal(perms.delete[0], 'acct:flash@gordon')
assert.equal(perms.admin[0], 'acct:flash@gordon')
it 'returns null if session.state.userid is falsey', ->
delete fakeSession.state.userid
assert.equal(permissions.shared("foo"), null)
describe 'default()', ->
it 'returns shared permissions if localStorage contains "shared"', ->
fakeLocalStorage.getItem = -> 'shared'
assert(permissions.isShared(permissions.default()))
it 'returns private permissions if localStorage contains "private"', ->
fakeLocalStorage.getItem = -> 'private'
assert(permissions.isPrivate(
permissions.default(), fakeSession.state.userid))
it 'returns shared permissions if localStorage is empty', ->
fakeLocalStorage.getItem = -> undefined
assert(permissions.isShared(permissions.default()))
describe 'setDefault()', ->
it 'sets the default permissions that default() will return', ->
stored = {}
fakeLocalStorage.setItem = (key, value) ->
stored[key] = value
fakeLocalStorage.getItem = (key) ->
return stored[key]
permissions.setDefault('private')
assert(permissions.isPrivate(
permissions.default(), fakeSession.state.userid))
permissions.setDefault('shared')
assert(permissions.isShared(
permissions.default('foo'), 'foo'))
describe 'isPublic', -> describe 'isPublic', ->
it 'isPublic() true if the read permission has group:__world__ in it', -> it 'isPublic() true if the read permission has group:__world__ in it', ->
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment