1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 05:09:42 +02:00

Improve Space permissions

Closes out loopholes that allowed managers to kick owners.
This commit is contained in:
McMatts 2018-11-16 19:18:10 +00:00
parent 09635b67ab
commit 5d632712e0
30 changed files with 1015 additions and 877 deletions

View file

@ -58,9 +58,9 @@ Space view.
## Latest Release ## Latest Release
[Community Edition: v1.76.0](https://github.com/documize/community/releases) [Community Edition: v1.76.1](https://github.com/documize/community/releases)
[Enterprise Edition: v1.76.0](https://documize.com/downloads) [Enterprise Edition: v1.76.1](https://documize.com/downloads)
## OS support ## OS support

View file

@ -504,8 +504,11 @@ func (h *Handler) FetchSpaceData(w http.ResponseWriter, r *http.Request) {
fetch := category.FetchSpaceModel{} fetch := category.FetchSpaceModel{}
// get space categories visible to user // get space categories visible to user
cat, err := h.Store.Category.GetBySpace(ctx, spaceID) var cat []category.Category
if err != nil && err != sql.ErrNoRows { var err error
cat, err = h.Store.Category.GetBySpace(ctx, spaceID)
if err != nil {
h.Runtime.Log.Error("get space categories visible to user failed", err) h.Runtime.Log.Error("get space categories visible to user failed", err)
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
return return
@ -527,7 +530,7 @@ func (h *Handler) FetchSpaceData(w http.ResponseWriter, r *http.Request) {
// get category membership records // get category membership records
member, err := h.Store.Category.GetSpaceCategoryMembership(ctx, spaceID) member, err := h.Store.Category.GetSpaceCategoryMembership(ctx, spaceID)
if err != nil && err != sql.ErrNoRows { if err != nil {
h.Runtime.Log.Error("get document category membership for space", err) h.Runtime.Log.Error("get document category membership for space", err)
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
return return

View file

@ -51,6 +51,8 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
return return
} }
org.StripSecrets()
response.WriteJSON(w, org) response.WriteJSON(w, org)
} }

View file

@ -34,7 +34,8 @@ type Store struct {
func (s Store) AddOrganization(ctx domain.RequestContext, o org.Organization) (err error) { func (s Store) AddOrganization(ctx domain.RequestContext, o org.Organization) (err error) {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_org (c_refid, c_company, c_title, c_message, c_domain, c_email, c_anonaccess, c_serial, c_maxtags, c_sub, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), _, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_org (c_refid, c_company, c_title, c_message, c_domain, c_email, c_anonaccess, c_serial, c_maxtags, c_sub, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
o.RefID, o.Company, o.Title, o.Message, strings.ToLower(o.Domain), o.RefID, o.Company, o.Title, o.Message, strings.ToLower(o.Domain),
strings.ToLower(o.Email), o.AllowAnonymousAccess, o.Serial, o.MaxTags, o.Subscription, o.Created, o.Revised) strings.ToLower(o.Email), o.AllowAnonymousAccess, o.Serial, o.MaxTags,
o.Subscription, o.Created, o.Revised)
if err != nil { if err != nil {
err = errors.Wrap(err, "unable to execute insert for org") err = errors.Wrap(err, "unable to execute insert for org")
@ -43,13 +44,14 @@ func (s Store) AddOrganization(ctx domain.RequestContext, o org.Organization) (e
return nil return nil
} }
// GetOrganization returns the Organization reocrod from the organization database table with the given id. // GetOrganization returns the Organization record from the organization database table with the given id.
func (s Store) GetOrganization(ctx domain.RequestContext, id string) (org org.Organization, err error) { func (s Store) GetOrganization(ctx domain.RequestContext, id string) (org org.Organization, err error) {
err = s.Runtime.Db.Get(&org, s.Bind(`SELECT id, c_refid AS refid, err = s.Runtime.Db.Get(&org, s.Bind(`SELECT id, c_refid AS refid,
c_title AS title, c_message AS message, c_domain AS domain, c_title AS title, c_message AS message, c_domain AS domain,
c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active, c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider, c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription, coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig,
coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
c_maxtags AS maxtags, c_created AS created, c_revised AS revised c_maxtags AS maxtags, c_created AS created, c_revised AS revised
FROM dmz_org FROM dmz_org
WHERE c_refid=?`), WHERE c_refid=?`),
@ -80,7 +82,8 @@ func (s Store) GetOrganizationByDomain(subdomain string) (o org.Organization, er
c_title AS title, c_message AS message, c_domain AS domain, c_title AS title, c_message AS message, c_domain AS domain,
c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active, c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider, c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription, coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig,
coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
c_maxtags AS maxtags, c_created AS created, c_revised AS revised c_maxtags AS maxtags, c_created AS created, c_revised AS revised
FROM dmz_org FROM dmz_org
WHERE c_domain=? AND c_active=true`), WHERE c_domain=? AND c_active=true`),
@ -95,7 +98,8 @@ func (s Store) GetOrganizationByDomain(subdomain string) (o org.Organization, er
c_title AS title, c_message AS message, c_domain AS domain, c_title AS title, c_message AS message, c_domain AS domain,
c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active, c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider, c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription, coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig,
coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
c_maxtags AS maxtags, c_created AS created, c_revised AS revised c_maxtags AS maxtags, c_created AS created, c_revised AS revised
FROM dmz_org FROM dmz_org
WHERE c_domain='' AND c_active=true`)) WHERE c_domain='' AND c_active=true`))

View file

@ -120,11 +120,6 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
return return
} }
url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name)))
me := false
hasEveryoneRole := false
roleCount := 0
// Permissions can be assigned to both groups and individual users. // Permissions can be assigned to both groups and individual users.
// Pre-fetch users with group membership to help us work out // Pre-fetch users with group membership to help us work out
// if user belongs to a group with permissions. // if user belongs to a group with permissions.
@ -136,6 +131,18 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
return return
} }
// url is sent in 'space shared with you' invitation emails.
url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name)))
// me tracks if the user who changed permissions, also has some space permissions.
me := false
// hasEveryRole tracks if "everyone" has been give access to space.
hasEveryoneRole := false
// hasOwner tracks is at least one person or user group has been marked as space owner.
hasOwner := false
// roleCount tracks the number of permission records created for this space.
// It's used to determine if space has multiple participants, see below.
roleCount := 0
for _, perm := range model.Permissions { for _, perm := range model.Permissions {
perm.OrgID = ctx.OrgID perm.OrgID = ctx.OrgID
perm.SpaceID = id perm.SpaceID = id
@ -154,6 +161,12 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
me = true me = true
} }
// Detect is we have at least one space owner permission.
// Result used below to prevent lock-outs.
if hasOwner == false && perm.SpaceOwner {
hasOwner = true
}
// Only persist if there is a role! // Only persist if there is a role!
if permission.HasAnyPermission(perm) { if permission.HasAnyPermission(perm) {
// identify publically shared spaces // identify publically shared spaces
@ -209,8 +222,11 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
} }
} }
// Do we need to ensure permissions for space owner when shared? // Catch and prevent lock-outs so we don't have
if !me { // zombie spaces that nobody can access.
if len(model.Permissions) == 0 {
// When no permissions are assigned we
// default to current user as being owner and viewer.
perm := permission.Permission{} perm := permission.Permission{}
perm.OrgID = ctx.OrgID perm.OrgID = ctx.OrgID
perm.Who = permission.UserPermission perm.Who = permission.UserPermission
@ -218,15 +234,37 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
perm.Scope = permission.ScopeRow perm.Scope = permission.ScopeRow
perm.Location = permission.LocationSpace perm.Location = permission.LocationSpace
perm.RefID = id perm.RefID = id
perm.Action = "" // we send array for actions below perm.Action = "" // we send allowable actions in function call...
err = h.Store.Permission.AddPermissions(ctx, perm, permission.SpaceOwner, permission.SpaceView)
err = h.Store.Permission.AddPermissions(ctx, perm, permission.SpaceView, permission.SpaceManage)
if err != nil { if err != nil {
ctx.Transaction.Rollback() ctx.Transaction.Rollback()
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)
return return
} }
} else {
// So we have permissions but we must check for at least one space owner.
if !hasOwner {
// So we have no space owner, make current user the owner
// if we have no permssions thus far.
if !me {
perm := permission.Permission{}
perm.OrgID = ctx.OrgID
perm.Who = permission.UserPermission
perm.WhoID = ctx.UserID
perm.Scope = permission.ScopeRow
perm.Location = permission.LocationSpace
perm.RefID = id
perm.Action = "" // we send allowable actions in function call...
err = h.Store.Permission.AddPermissions(ctx, perm, permission.SpaceOwner, permission.SpaceView)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
}
} }
// Mark up space type as either public, private or restricted access. // Mark up space type as either public, private or restricted access.

View file

@ -116,24 +116,35 @@ func (s Store) GetViewable(ctx domain.RequestContext) (sp []space.Space, err err
return return
} }
// GetAll for admin users! // AdminList returns all shared spaces and orphaned spaces that have no owner.
func (s Store) GetAll(ctx domain.RequestContext) (sp []space.Space, err error) { func (s Store) AdminList(ctx domain.RequestContext) (sp []space.Space, err error) {
qry := s.Bind(`SELECT id, c_refid AS refid, qry := s.Bind(`
SELECT id, c_refid AS refid,
c_name AS name, c_orgid AS orgid, c_userid AS userid, c_name AS name, c_orgid AS orgid, c_userid AS userid,
c_type AS type, c_lifecycle AS lifecycle, c_likes AS likes, c_type AS type, c_lifecycle AS lifecycle, c_likes AS likes,
c_created AS created, c_revised AS revised c_created AS created, c_revised AS revised
FROM dmz_space FROM dmz_space
WHERE c_orgid=? WHERE c_orgid=? AND (c_type=? OR c_type=?)
ORDER BY c_name`) UNION ALL
SELECT id, c_refid AS refid,
err = s.Runtime.Db.Select(&sp, qry, ctx.OrgID) c_name AS name, c_orgid AS orgid, c_userid AS userid,
c_type AS type, c_lifecycle AS lifecycle, c_likes AS likes,
c_created AS created, c_revised AS revised
FROM dmz_space
WHERE c_orgid=? AND (c_type=? OR c_type=?) AND c_refid NOT IN
(SELECT c_refid FROM dmz_permission WHERE c_orgid=? AND c_action='own')
ORDER BY name`)
err = s.Runtime.Db.Select(&sp, qry,
ctx.OrgID, space.ScopePublic, space.ScopeRestricted,
ctx.OrgID, space.ScopePublic, space.ScopeRestricted,
ctx.OrgID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
err = nil err = nil
sp = []space.Space{} sp = []space.Space{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("failed space.GetAll org %s", ctx.OrgID)) err = errors.Wrap(err, fmt.Sprintf("failed space.AdminList org %s", ctx.OrgID))
} }
return return

View file

@ -59,9 +59,9 @@ type SpaceStorer interface {
Get(ctx domain.RequestContext, id string) (sp space.Space, err error) Get(ctx domain.RequestContext, id string) (sp space.Space, err error)
PublicSpaces(ctx domain.RequestContext, orgID string) (sp []space.Space, err error) PublicSpaces(ctx domain.RequestContext, orgID string) (sp []space.Space, err error)
GetViewable(ctx domain.RequestContext) (sp []space.Space, err error) GetViewable(ctx domain.RequestContext) (sp []space.Space, err error)
GetAll(ctx domain.RequestContext) (sp []space.Space, err error)
Update(ctx domain.RequestContext, sp space.Space) (err error) Update(ctx domain.RequestContext, sp space.Space) (err error)
Delete(ctx domain.RequestContext, id string) (rows int64, err error) Delete(ctx domain.RequestContext, id string) (rows int64, err error)
AdminList(ctx domain.RequestContext) (sp []space.Space, err error)
} }
// CategoryStorer defines required methods for category and category membership management // CategoryStorer defines required methods for category and category membership management

View file

@ -41,8 +41,8 @@ func main() {
rt.Product = domain.Product{} rt.Product = domain.Product{}
rt.Product.Major = "1" rt.Product.Major = "1"
rt.Product.Minor = "76" rt.Product.Minor = "76"
rt.Product.Patch = "0" rt.Product.Patch = "1"
rt.Product.Revision = 181113144232 rt.Product.Revision = 181116171324
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch) rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
rt.Product.Edition = domain.CommunityEdition rt.Product.Edition = domain.CommunityEdition
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition) rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)

File diff suppressed because one or more lines are too long

View file

@ -22,6 +22,7 @@ export default Component.extend(Notifier, Modals, {
subscription: null, subscription: null,
planCloud: false, planCloud: false,
planSelfhost: false, planSelfhost: false,
comment: 'Nothing in particular -- just passing through. Please close my Documize account.',
didReceiveAttrs() { didReceiveAttrs() {
this._super(...arguments); this._super(...arguments);
@ -47,10 +48,17 @@ export default Component.extend(Notifier, Modals, {
}); });
}, },
onRequestClosure() {
this.modalOpen("#deactivation-request-modal", {"show": true}, '#close-comment');
},
onDeactivate() { onDeactivate() {
this.get('global').deactivate().then(() => { this.modalClose("#deactivation-request-modal");
let comment = this.get('comment');
this.get('global').deactivate(comment).then(() => {
this.showDone(); this.showDone();
this.modalOpen("#deactivation-modal", {"show": true}); this.modalOpen("#deactivation-confirmation-modal", {"show": true});
}); });
} }
} }

View file

@ -0,0 +1,105 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import $ from 'jquery';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import Notifier from '../../mixins/notifier';
import Modals from '../../mixins/modal';
import Component from '@ember/component';
export default Component.extend(Notifier, Modals, {
spaceSvc: service('folder'),
browserSvc: service('browser'),
documentSvc: service('document'),
spaces: null,
label: computed('model', function() {
switch (this.get('model').length) {
case 1:
return "space";
default:
return "spaces";
}
}),
init() {
this._super(...arguments);
this.loadData();
},
didReceiveAttrs() {
this._super(...arguments);
this.deleteSpace = {
id: '',
name: ''
};
},
loadData() {
this.get('spaceSvc').manage().then((s) => {
this.set('spaces', s);
});
},
actions: {
onShow(id) {
this.set('deleteSpace.id', id);
},
onDelete() {
let deleteSpace = this.get('deleteSpace');
let spaceId = deleteSpace.id;
let spaceNameTyped = deleteSpace.name;
let space = this.get('spaces').findBy('id', spaceId);
let spaceName = space.get('name');
if (spaceNameTyped !== spaceName || spaceNameTyped === '' || spaceName === '') {
$('#delete-space-name').addClass('is-invalid').focus();
return;
}
$('#space-delete-modal').modal('hide');
$('#space-delete-modal').modal('dispose');
this.get('spaceSvc').delete(spaceId).then(() => { /* jshint ignore:line */
this.set('deleteSpace.id', '');
this.set('deleteSpace.name', '');
this.loadData();
});
},
onExport() {
this.showWait();
let spec = {
spaceId: '',
data: _.pluck(this.get('folders'), 'id'),
filterType: 'space',
};
this.get('documentSvc').export(spec).then((htmlExport) => {
this.get('browserSvc').downloadFile(htmlExport, 'documize.html');
this.showDone();
});
},
onOwner(spaceId) {
this.showWait();
this.get('spaceSvc').grantOwnerPermission(spaceId).then(() => { /* jshint ignore:line */
this.showDone();
});
}
}
});

View file

@ -62,6 +62,15 @@ export default Component.extend(ModalMixin, TooltipMixin, Notifer, {
cat.set('documents', docCount); cat.set('documents', docCount);
cat.set('users', userCount); cat.set('users', userCount);
}); });
this.get('categorySvc').getUserVisible(this.get('space.id')).then((cm) => {
cm.forEach((cm) => {
let cat = _.findWhere(c, {id: cm.get('id') });
if (is.not.undefined(cat)) {
cat.set('access', is.not.undefined(cat));
}
});
});
}); });
}); });
}, },

View file

@ -34,6 +34,9 @@ export default Component.extend(Notifier, Modals, {
isSpaceAdmin: computed('permissions', function() { isSpaceAdmin: computed('permissions', function() {
return this.get('permissions.spaceOwner') || this.get('permissions.spaceManage'); return this.get('permissions.spaceOwner') || this.get('permissions.spaceManage');
}), }),
isNotSpaceOwner: computed('permissions', function() {
return !this.get('permissions.spaceOwner');
}),
didReceiveAttrs() { didReceiveAttrs() {
this._super(...arguments); this._super(...arguments);

View file

@ -29,7 +29,6 @@ export default Component.extend(AuthMixin, {
init() { init() {
this._super(...arguments); this._super(...arguments);
// this.filteredDocs = [];
this.setup(); this.setup();
}, },

View file

@ -9,85 +9,7 @@
// //
// https://documize.com // https://documize.com
import $ from 'jquery';
import { computed } from '@ember/object';
import { inject as service } from '@ember/service';
import Notifier from '../../../mixins/notifier';
import TooltipMixin from '../../../mixins/tooltip';
import Controller from '@ember/controller'; import Controller from '@ember/controller';
export default Controller.extend(TooltipMixin, Notifier, { export default Controller.extend({
folderService: service('folder'),
browserSvc: service('browser'),
documentSvc: service('document'),
dropdown: null,
init() {
this._super(...arguments);
this.folders = [];
this.deleteSpace = {
id: '',
name: ''
};
},
label: computed('folders', function() {
switch (this.get('folders').length) {
case 1:
return "space";
default:
return "spaces";
}
}),
actions: {
onShow(id) {
this.set('deleteSpace.id', id);
},
onDelete() {
let deleteSpace = this.get('deleteSpace');
let spaceId = deleteSpace.id;
let spaceNameTyped = deleteSpace.name;
let space = this.get('folders').findBy('id', spaceId);
let spaceName = space.get('name');
if (spaceNameTyped !== spaceName || spaceNameTyped === '' || spaceName === '') {
$('#delete-space-name').addClass('is-invalid').focus();
return;
}
$('#space-delete-modal').modal('hide');
$('#space-delete-modal').modal('dispose');
this.get('folderService').delete(spaceId).then(() => { /* jshint ignore:line */
this.set('deleteSpace.id', '');
this.set('deleteSpace.name', '');
this.get('folderService').adminList().then((folders) => {
let nonPrivateFolders = folders.rejectBy('spaceType', 2);
if (is.empty(nonPrivateFolders) || is.null(folders) || is.undefined(folders)) {
nonPrivateFolders = [];
}
this.set('folders', nonPrivateFolders);
});
});
},
onExport() {
this.showWait();
let spec = {
spaceId: '',
data: _.pluck(this.get('folders'), 'id'),
filterType: 'space',
};
this.get('documentSvc').export(spec).then((htmlExport) => {
this.get('browserSvc').downloadFile(htmlExport, 'documize.html');
this.showDone();
});
}
}
}); });

View file

@ -9,33 +9,17 @@
// //
// https://documize.com // https://documize.com
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
import Route from '@ember/routing/route';
export default Route.extend(AuthenticatedRouteMixin, { export default Route.extend(AuthenticatedRouteMixin, {
folderService: service('folder'),
beforeModel() { beforeModel() {
if (!this.session.isAdmin) { if (!this.session.isAdmin) {
this.transitionTo('auth.login'); this.transitionTo('auth.login');
} }
}, },
model() {
return this.get('folderService').adminList();
},
setupController(controller, model) {
let nonPrivateFolders = model.rejectBy('spaceType', 2);
if (is.empty(nonPrivateFolders) || is.null(model) || is.undefined(model)) {
nonPrivateFolders = [];
}
controller.set('folders', nonPrivateFolders);
},
activate() { activate() {
this.get('browser').setTitle('Spaces'); this.get('browser').setTitle('Manage Spaces');
} }
}); });

View file

@ -1,50 +1 @@
{{#if folders}} {{customize/space-admin}}
<div class="row">
<div class="col">
<div class="view-customize">
<h1 class="admin-heading">{{folders.length}} shared {{label}}</h1>
<button type="button" class="btn btn-success" onclick={{action 'onExport'}}>Export as HTML</button>
</div>
</div>
</div>
<div class="view-customize">
<div class="space-list">
{{#each folders as |folder|}}
<div class="space row">
<div class="col-12 col-sm-8">
{{#link-to 'folder' folder.id folder.slug class="alt"}}{{folder.name}}{{/link-to}}
</div>
<div class="col-12 col-sm-4 text-right">
<div id="space-delete-button-{{folder.id}}" class="button-icon-danger align-middle" data-toggle="tooltip" data-placement="top" title="Delete space" {{action "onShow" folder.id}}>
<i class="material-icons" data-toggle="modal" data-target="#space-delete-modal" data-backdrop="static">delete</i>
</div>
</div>
</div>
{{/each}}
</div>
</div>
<div id="space-delete-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">Space Deletion</div>
<div class="modal-body">
<form onsubmit={{action 'onDelete'}}>
<p>Are you sure you want to delete this space and all documents?</p>
<div class="form-group">
<label for="delete-space-name">Please type space name to confirm</label>
{{input type='text' id="delete-space-name" class="form-control mousetrap" placeholder="Space name" value=deleteSpace.name}}
<small class="form-text text-muted">This will delete all documents and templates within this space!</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" onclick={{action 'onDelete'}}>Delete</button>
</div>
</div>
</div>
</div>
{{else}}
<p>There are no spaces to maintain</p>
{{/if}}

View file

@ -84,7 +84,7 @@ export default Controller.extend(Tooltips, Notifier, {
let constants = this.get('constants'); let constants = this.get('constants');
// if document approval mode is locked return // if document approval mode is locked return
if (document.get('protection') == constants.ProtectionType.Lock) { if (document.get('protection') === constants.ProtectionType.Lock) {
// should not really happen // should not really happen
return; return;
} }

View file

@ -335,7 +335,7 @@ export default Service.extend({
} else { } else {
let id = this.get('storageSvc').getSessionItem('anonId'); let id = this.get('storageSvc').getSessionItem('anonId');
if (is.not.null(id) && is.not.undefined(id) && id.length === 16) { if (is.not.null(id) && is.not.undefined(id) && id.length >= 16) {
userId = id; userId = id;
} else { } else {
userId = stringUtil.anonUserId(); userId = stringUtil.anonUserId();

View file

@ -174,8 +174,9 @@ export default BaseService.extend({
}); });
}, },
// returns all spaces -- for use by documize admin user // Returns all shared spaces and spaces without an owner.
adminList() { // Administrator only method.
manage() {
return this.get('ajax').request(`space/manage`, { return this.get('ajax').request(`space/manage`, {
method: "GET" method: "GET"
}).then((response) => { }).then((response) => {
@ -189,5 +190,12 @@ export default BaseService.extend({
return data; return data;
}); });
} },
// Add admin as space owner.
grantOwnerPermission(folderId) {
return this.get('ajax').request(`space/manage/owner/${folderId}`, {
method: 'POST',
});
},
}); });

View file

@ -236,10 +236,12 @@ export default Service.extend({
}); });
}, },
deactivate() { deactivate(comment) {
if(this.get('sessionService.isAdmin')) { if(this.get('sessionService.isAdmin')) {
return this.get('ajax').request(`deactivate`, { return this.get('ajax').request(`deactivate`, {
method: 'POST', method: 'POST',
contentType: 'text',
data: comment,
}).then(() => { }).then(() => {
return; return;
}); });

View file

@ -65,13 +65,11 @@
> .space-list { > .space-list {
padding: 0; padding: 0;
margin: 0; margin: 3rem 0;
> .space { > .space {
margin: 15px 0; margin: 15px 0;
padding: 15px; padding: 15px 0;
@include card();
@include ease-in();
font-size: 1.2rem; font-size: 1.2rem;
color: $color-primary; color: $color-primary;
} }

View file

@ -143,7 +143,7 @@
<p>Requests can take up to 24 hours to process.</p> <p>Requests can take up to 24 hours to process.</p>
{{#link-to 'customize.backup' class="btn btn-success"}}PERFORM BACKUP{{/link-to}} {{#link-to 'customize.backup' class="btn btn-success"}}PERFORM BACKUP{{/link-to}}
<div class="button-gap" /> <div class="button-gap" />
<button class="btn btn-danger" {{action 'onDeactivate'}}>REQUEST ACCOUNT CLOSURE</button> <button class="btn btn-danger" {{action 'onRequestClosure'}}>CLOSE ACCOUNT</button>
</div> </div>
</div> </div>
</div> </div>
@ -151,16 +151,37 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
<div id="deactivation-modal" class="modal" tabindex="-1" role="dialog"> <div id="deactivation-request-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">Request Account Closure</div>
<div class="modal-body">
<form {{action "onEditComment" on="submit"}}>
<div class="form-group">
<label for="the-comment">Comment</label>
{{focus-textarea id="close-comment" class="form-control" rows="5" value=comment}}
<small class="form-text text-muted">We always welcome product feedback</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" onclick={{action 'onDeactivate'}}>Close Account</button>
</div>
</div>
</div>
</div>
<div id="deactivation-confirmation-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header">Deactivation Requested</div> <div class="modal-header">Deactivation Requested</div>
<div class="modal-body"> <div class="modal-body">
<p>Your request has been sent and will be processed shortly.</p> <p>Your request has been sent and will be processed shortly.</p>
<p>If you haven't already, perform a backup to download all your data.</p> <p>If you haven't already, please run a backup to download all your data.</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">ok</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,58 @@
<div class="row">
<div class="col">
<div class="view-customize">
<h1 class="admin-heading">Manage Spaces</h1>
<h2 class="sub-heading">Delete spaces, take ownership of shared spaces and orphaned spaces</h2>
</div>
</div>
</div>
<div class="view-customize my-5">
{{#if spaces}}
<button type="button" class="btn btn-success" onclick={{action 'onExport'}}>Export content</button>
<div class="space-list">
{{#each spaces as |space|}}
<div class="space row">
<div class="col-12 col-sm-8">
{{#link-to 'folder' space.id space.slug class="alt"}}{{space.name}}{{/link-to}}
</div>
<div class="col-12 col-sm-4 text-right">
<div id="space-ownership-button-{{space.id}}" class="button-icon-gray align-middle" data-toggle="tooltip" data-placement="top" title="Add myself as space owner" {{action "onOwner" space.id}}>
<i class="material-icons" data-toggle="modal">person_add</i>
</div>
<div class="button-icon-gap" />
<div id="space-delete-button-{{space.id}}" class="button-icon-danger align-middle" data-toggle="tooltip" data-placement="top" title="Delete space and all content" {{action "onShow" space.id}}>
<i class="material-icons" data-toggle="modal" data-target="#space-delete-modal" data-backdrop="static">delete</i>
</div>
</div>
</div>
{{/each}}
</div>
<div id="space-delete-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">Space Deletion</div>
<div class="modal-body">
<form onsubmit={{action 'onDelete'}}>
<p>Are you sure you want to delete this space and all documents?</p>
<div class="form-group">
<label for="delete-space-name">Please type space name to confirm</label>
{{input type='text' id="delete-space-name" class="form-control mousetrap" placeholder="Space name" value=deleteSpace.name}}
<small class="form-text text-muted">This will delete all documents and templates within this space!</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" onclick={{action 'onDelete'}}>Delete</button>
</div>
</div>
</div>
</div>
{{else}}
<p>There are no shared spaces to manage</p>
{{/if}}
</div>

View file

@ -19,7 +19,10 @@
{{else}} {{else}}
<div class="category col-8"> <div class="category col-8">
<div class="name">{{cat.category}}</div> <div class="name">{{cat.category}}</div>
<div class="info">{{cat.documents}} {{if (eq cat.documents 1) 'document' 'documents' }} &middot; {{cat.users}} users/groups</div> <div class="info">
{{cat.documents}} {{if (eq cat.documents 1) 'document' 'documents' }} &middot; {{cat.users}} users/groups
{{#unless cat.access}}<span class="text-danger">(you have no view permission)</span>{{/unless}}
</div>
</div> </div>
{{/if}} {{/if}}
<div class="col-4 buttons text-right"> <div class="col-4 buttons text-right">

View file

@ -74,7 +74,7 @@
</td> </td>
<td>{{x-toggle value=permission.spaceView onToggle=(action (mut permission.spaceView))}}</td> <td>{{x-toggle value=permission.spaceView onToggle=(action (mut permission.spaceView))}}</td>
<td>{{x-toggle value=permission.spaceManage onToggle=(action (mut permission.spaceManage))}}</td> <td>{{x-toggle value=permission.spaceManage onToggle=(action (mut permission.spaceManage))}}</td>
<td>{{x-toggle value=permission.spaceOwner onToggle=(action (mut permission.spaceOwner))}}</td> <td>{{x-toggle value=permission.spaceOwner onToggle=(action (mut permission.spaceOwner)) disabled=isNotSpaceOwner}}</td>
<td>{{x-toggle value=permission.documentAdd onToggle=(action (mut permission.documentAdd))}}</td> <td>{{x-toggle value=permission.documentAdd onToggle=(action (mut permission.documentAdd))}}</td>
<td>{{x-toggle value=permission.documentEdit onToggle=(action (mut permission.documentEdit))}}</td> <td>{{x-toggle value=permission.documentEdit onToggle=(action (mut permission.documentEdit))}}</td>
<td>{{x-toggle value=permission.documentDelete onToggle=(action (mut permission.documentDelete))}}</td> <td>{{x-toggle value=permission.documentDelete onToggle=(action (mut permission.documentDelete))}}</td>

View file

@ -1,6 +1,6 @@
{ {
"name": "documize", "name": "documize",
"version": "1.76.0", "version": "1.76.1",
"description": "The Document IDE", "description": "The Document IDE",
"private": true, "private": true,
"repository": "", "repository": "",

View file

@ -95,6 +95,7 @@ const (
EventTypeWorkflowPublishRequested EventType = "requested-publication" EventTypeWorkflowPublishRequested EventType = "requested-publication"
EventTypeDatabaseBackup EventType = "backedup-database" EventTypeDatabaseBackup EventType = "backedup-database"
EventTypeDatabaseRestore EventType = "restored-database" EventTypeDatabaseRestore EventType = "restored-database"
EventTypeAssumedSpaceOwnership EventType = "assumed-space-ownership"
// EventTypeVersionAdd records addition of version // EventTypeVersionAdd records addition of version
EventTypeVersionAdd EventType = "added-version" EventTypeVersionAdd EventType = "added-version"

View file

@ -11,7 +11,9 @@
package org package org
import "github.com/documize/community/model" import (
"github.com/documize/community/model"
)
// Organization defines a tenant that uses this app. // Organization defines a tenant that uses this app.
type Organization struct { type Organization struct {
@ -28,5 +30,10 @@ type Organization struct {
MaxTags int `json:"maxTags"` MaxTags int `json:"maxTags"`
Serial string `json:"serial"` Serial string `json:"serial"`
Active bool `json:"active"` Active bool `json:"active"`
Subscription string Subscription string `json:"subscription"`
}
// StripSecrets removes sensitive information.
func (o *Organization) StripSecrets() {
o.Subscription = ""
} }

View file

@ -131,7 +131,8 @@ func RegisterEndpoints(rt *env.Runtime, s *store.Store) {
AddPrivate(rt, "space/{spaceID}", []string{"DELETE", "OPTIONS"}, nil, space.Delete) AddPrivate(rt, "space/{spaceID}", []string{"DELETE", "OPTIONS"}, nil, space.Delete)
AddPrivate(rt, "space/{spaceID}/move/{moveToId}", []string{"DELETE", "OPTIONS"}, nil, space.Remove) AddPrivate(rt, "space/{spaceID}/move/{moveToId}", []string{"DELETE", "OPTIONS"}, nil, space.Remove)
AddPrivate(rt, "space/{spaceID}/invitation", []string{"POST", "OPTIONS"}, nil, space.Invite) AddPrivate(rt, "space/{spaceID}/invitation", []string{"POST", "OPTIONS"}, nil, space.Invite)
AddPrivate(rt, "space/manage", []string{"GET", "OPTIONS"}, nil, space.GetAll) AddPrivate(rt, "space/manage", []string{"GET", "OPTIONS"}, nil, space.Manage)
AddPrivate(rt, "space/manage/owner/{spaceID}", []string{"POST", "OPTIONS"}, nil, space.ManageOwner)
AddPrivate(rt, "space/{spaceID}", []string{"GET", "OPTIONS"}, nil, space.Get) AddPrivate(rt, "space/{spaceID}", []string{"GET", "OPTIONS"}, nil, space.Get)
AddPrivate(rt, "space", []string{"GET", "OPTIONS"}, nil, space.GetViewable) AddPrivate(rt, "space", []string{"GET", "OPTIONS"}, nil, space.GetViewable)
AddPrivate(rt, "space/{spaceID}", []string{"PUT", "OPTIONS"}, nil, space.Update) AddPrivate(rt, "space/{spaceID}", []string{"PUT", "OPTIONS"}, nil, space.Update)