Google Sheets - Allow Users to run scripts on a protected range - google-apps-script

I'm new to Apps Script and have been slowing teaching myself. I have searched Stack Over Flow and found similar questions and tried to work through the issue using the information found but thus far with no success. Hopefully someone here can give a newbie some guidance.
I have a spreadsheet that I am sharing with other users. There are a few ranges with formulas in them that I am trying to protect. I have written a script that can change the forumlas within the protected ranges. It runs perfectly under my account but the shared users are unable to run the script because they "don't have permission".
I have tried to write a bit of code that tries to grant temporary access to edit the protected ranges. My thought was to have three different functions. The first function removes the protections. The second function performs the changes to the now unprotected ranges. The third function then protects the ranges again. The code below works perfectly under my account. However when logged in as a shared user it causes an alert saying that "You do not have permission to perform.."
Can someone please tell me where I am going wrong and point me in the right direction?
function myFunction() {
//add protections back
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Spray Template');
var range = sheet.getRange('B15:E15');
var range2 = sheet.getRange('G15:H15');
var range3 = sheet.getRange('D16:E16');
var ranges = [range,range2,range3];
for (var j = 0; j<3; j++){
ranges[j].protect().removeEditor('email_here#gmail.com');
}
}
function myFunction2(){
//remove protections
var ss = SpreadsheetApp.getActive();
var ss2= ss.getSheetByName('Spray Template');
var protections = ss2.getProtections(SpreadsheetApp.ProtectionType.RANGE);
f
or (var i = 0; i < protections.length; i++) {
var protection = protections[i];
protection.addEditor('email_here#gmail.com');
SpreadsheetApp.flush();
protection.remove();
}
}
function test() {
myFunction2();
functionThatChangesRange();
myFunction();
}

I imagine that without permission to edit/access those protected ranges, other people will also lack permission to edit the protection itself (even through scripts).
This is because the script inherits its permissions from the user that is running it (ie the script runs under that person's account).
Obviously, you can't both protect a range, and also allow anyone to change the protection - that would defeat the whole purpose!

I saw it suggested elsewhere to leave one cell unprotected and setup a script within the document that runs 'onedit'. Whenever the user changes the value of that cell the script should run.

One can use onEdit to run the script or installable trigger. However, I have not identified a way in which a script can be called from a custom menu or a button and edit successfully protected ranges.

Related

Error when sharing Sheet with Google Script

When sharing a Google Sheet with a button that runs a Google Script I run into the following problem:
- When anyone but myself clicks the button the script will return the following error:
"You are trying to edit a protected cell or object. Please contact the spreadsheet owner to remove protection if you""
I had a couple protected ranges (deleted them now) in the sheet, but not one even close to the button.
I've tried to add a button on one of the shared user's account and copied the script into a new script file (re-linking the script created/copied by the shared user to the button created by the shared user), but to no avail.
Anyone who knows the solution to this problem ?
Removing the protection doesn't work just by deleting the cell values. There's a sample code in Class Protection which shows adding and removing protection in sheet ranges:
// Remove all range protections in the spreadsheet that the user has permission to edit.
var ss = SpreadsheetApp.getActive();
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
if (protection.canEdit()) {
protection.remove();
}
}

Installed onEdit trigger does not

I have a function that is intended to detect changes in one spreadsheet and copy them into another spreadsheet in the same exact cell location if the change is made in sheet1. I first tried to do this using the installed onEdit(e) function provided by sheetsAPI, but kept running into the error that I'm not authorized to open a new spreadsheet from a built-in function. I tried setting up my own trigger, but it won't activate even though I assigned an OnEdit trigger to the function.
function ChangeDetect(e){
var range = e.range
var esheet = SpreadsheetApp.getActiveSheet()
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sheet = ss.getSheets()[1];
// literally, if the edit was made in the first sheet of the spreadsheet, do the following:
if (esheet.getIndex() == ss.getSheets()[0].getIndex()){
var numrows = range.getNumRows();
var numcols = range.getNumColumns();
for (var i = 1; i <= numrows; i++) {
for (var j = 1; j <= numcols; j++) {
var currentValue = range.getCell(i,j).getValue();
var cellRow = range.getCell(i,j).getRow();
var cellCol = range.getCell(i,j).getColumn();
var ss3 = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/1K2ifzjPTATH77tV4xInh0Ga2SuPsLdgNRSbekjDx-w8/edit#gid=0')
var sheet3 = ss3.getSheets()[0];
sheet3.getRange(cellRow,cellCol).setValue(currentValue)
}
}
}
}
I think you may be experiencing the limitation/restrictions of installable triggers:
Restrictions
Because simple triggers fire automatically, without asking the user for authorization, they are subject to several restrictions:
The script must be bound to a Google Sheets, Docs, or Forms file.
They do not run if a file is opened in read-only (view or comment) mode.
They cannot access services that require authorization. For example, a simple trigger cannot send an email because the Gmail service requires authorization, but a simple trigger can translate a phrase with the -
Language service, which is anonymous.
They can modify the file they are bound to, but cannot access other files because that would require authorization.
They may or may not be able to determine the identity of the current user, depending on a complex set of security restrictions.
They cannot run for longer than 30 seconds.
In certain circumstances, add-ons for Google Sheets, Docs, and Forms run - their onOpen(e) and onEdit(e) simple triggers in a no-authorization mode that presents some additional complications. For more information, see the guide to the add-on authorization lifecycle.
These restrictions do not apply to doGet() or doPost().

Automating Permissions and I keep getting an extra person in there

So I hate permissions, they are awful. BUT I also hate it when people over write things they didn't realise were important... I was really excited to find out that I could automate the entire granting permissions ordeal using google scripting.
So I tested things out in a small dummy sheet, and got it working the way I wanted to. However! When I use it on my test template of the actual sheet I will need to be using it on, it will (for some reason) add the other person I shared the file with into all the levels of permissions.
This is not what I want. The other person is currently me (at SHOULDN'TBEINEVERYTHING#WHYYYYY.com in this script), but that account is supposed to have the lower level of permissions so I can make sure that everything is working the way I want it to. Here's a dummy version of the code I'm using:
function plus10Protection() {
var ss = SpreadsheetApp.getActive();
// Removes protections from Ranges
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
if (protection.canEdit()) {
protection.remove();
}
}
//removes sheet protections
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.SHEET);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
if (protection.canEdit()) {
protection.remove();
}
}
//SHEET1
var sh7 = ss.getSheetByName("Sheet1");
var protection = sh7.protect().setDescription('Sheet1 - Wizards');
//protects whole sheet
protection.addEditors(['wizard1#test.com',
'wizard2#test.com']);
//adds email addresses to WIZARD range - SHEET1
var unprotected = sh7.getRange('A1:D');
protection.setUnprotectedRanges([sh7.getRange("B1:B1"),
sh7.getRange("B3:D")]);
//unprotects
var range = sh7.getRange('B1:B1'); //selects the range
var protection = range.protect().setDescription('Sheet1 - Part Number');
//names the range
var me = Session.getEffectiveUser();
protection.addEditors(['wizard1#test.com',
'wizard2#test.com',
'supervisor1#test.com']);
//put emails here SHEET1 SUPERVISORS
var range = sh7.getRange('B3:D'); //selects the range
var protection = range.protect().setDescription('Sheet1 - Workers');
//names the range
var me = Session.getEffectiveUser();
protection.addEditors(['wizard1#test.com',
'wizard2#test.com',
'supervisor1#test.com'
'worker1#test.com'
'worker2#test.com'
'SHOULDN'TBEINEVERYTHING#WHYYYYY.com']);
//put emails here SHEET1 WORKERS
Repeated for about fifteen other tabs. I was really wanting this to solve the problem with updating permissions when people leave or join, since this script removes old permissions and then puts new ones on. However, the sheet is already shared with everyone at the company, and if suddenly everyone has Wizard level access, it's pretty much the worst.
I tested it again by sharing with another person, and they were added in to everything as well. Is there some code snippet that specifies only sharing with the emails I've provided, and not everyone it could possibly share with? (stop getting over excited, google.)
I don't want to have to un-share it with everyone and then re-share, because that seems like a huge problem. I'm hoping there's something in the code that I stole from somewhere online (I should have bookmarked it!!!) is actually calling everyone with edit permissions.... Maybe the getEffectiveUser? Though I'm running it from my test1#test.com account, not from the other which is open in a different browser! (Firefox, and I'm scripting in Chrome.)
I also tested to see if maybe it was because I was signed in and looking at the sheet with my SHOULDN'TBEINEVERYTHING#WHYYYYY.com account, but nope, that's not it.
Is it possible to script sharing? So I could have it first unshare with everyone, clear the permissions, set the new permissions, then re-share? If that is the only solution? The less finicky I can make this the better. :S
I'm trying to get answers to this problem over on the google sheets forums, but no bites so far. I was hoping to cast a wider net.
Still haven't figured out how to change a user's permission level, but thanks to some code I found (which I thought I had bookmarked, but now can't find) I can remove everyone's permissions, run my protection script, then share with everyone again.
//removes all Editors
var SpreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var editors = SpreadSheet.getEditors();
for (var i = 0; i < editors.length; i++) {
SpreadSheet.removeEditor(editors[i]);
};
and then
//Adds all editors back.
var SpreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var editors = SpreadSheet.getEditors();
SpreadSheet.addEditors(['PUT EMAILS HERE', 'separated by commas']);
I need to find out how to add the person running the script to the permissions specifically, just in case it isn't the owner running the script. A task for tonight!

Processing apply protection on multiple ranges all together

Recently ability to manipulate protection for a range programatically has been added. But is there a way to add multiple ranges all together when they have same protection to apply protection instead of processing each range one at a time? The following code is from Google sample script. Currently I am running the following code as many as ranges I have. Is there a better way to do it all at once?
var ss = SpreadsheetApp.getActive();
var range = ss.getRange('A1:B10');
var protection = range.protect().setDescription('Sample protected range');
// Protect the active sheet except B2:C5, then remove all other users from the list of editors.
var sheet = SpreadsheetApp.getActiveSheet();
var protection = sheet.protect().setDescription('Sample protected sheet');
var unprotected = sheet.getRange('B2:C5');
protection.setUnprotectedRanges([unprotected]);
// Ensure the current user is an editor before removing others. Otherwise, if the user's edit
// permission comes from a group, the script will throw an exception upon removing the group.
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
Thanks.
The brackets[] in:
protection.setUnprotectedRanges([unprotected]);
suggests that an array is expected. Have you tried:
protection.setUnprotectedRanges([sheet.getRange("B2:B5"),
sheet.getRange("F2:F5"),
sheet.getRange("H2:H5")]);
It is not possible to manipulate non-contiguous cells in Google Spreadsheet (yet?), but there are optimiziations that can be done, setting the ranges asynchronously with HTML calls, threaded, protecting all then unprotecting some, all depends on your case, can you give an example on how your using it, how much cells you're protecting?
Updating:
To do asynchronous HTML calls you would first create a sideBar or a modelessDialog, and make a button that execute many google.script.run functions. Since the code in the HTML page is asynchronous (it continous to run even though the function in google servers isn't done), you can make dozens of calls, E.G.:
In javascript HTML:
function protectOdds(){
for(var i = 0; i < 10; i++)
google.script.run.protectCells( i );
}
This would run alot faster than calling trough the .gs code to protect one at a time.
It might be a little late, but here is what I did.
Var ranges = [ss.getRange('A1:B10'), ss.getRange('C1:E10')];
for (var i = 0; i < ranges.length; i++){
var protection = ranges[i].protect().setDescription('Sample protected range');
}

GAS: Range Protection?

I've looked everywhere and it seems that GAS hasn't caught up with Google Spreadsheet. Is there a brute force method for setting protection on certain ranges in a sheet..? (And making a protected sheet with all the formulas and referring to them won't help me.)
I found this through google: https://code.google.com/p/google-apps-script-issues/issues/detail?id=1721
I even commented at the bottom. (More of a complaint than anything useful.) But the guy above me there posted this code:
//Function to Protect Target Sheet
function ProtectTargetSheet() {
//Enter ID for each Worksheet
var IDs = ["Sheeet_1", "Sheet_2"]
//Enter Page to protect, in order of WorkSheets
var Sheet_names = ["Page_1", "Page_2"]
//For each sheet in the array
for ( i = 0; i < IDs.length; i ++) {
//id of sheet
var sheet = IDs[i]
//activate dedicated sheet
var ActiveSheet = SpreadsheetApp.openById(sheet)
//Find last row and column for each sheet
var LastRow = ActiveSheet.getSheetByName(Sheet_names[i]).getLastRow();
var LastCol = ActiveSheet.getSheetByName(Sheet_names[i]).getLastColumn();
//Name of protectSheet
var Name = "Protect_Sheet";
//Range for Protection
var Named_Range = ActiveSheet.getSheetByName(Sheet_names[i]).getRange(1, 1, LastRow, LastCol);
//Impletment Protect Range
var protected_Range = ActiveSheet.setNamedRange(Name, Named_Range);
}
}
I don't see how this can work to give protection to a range when shared. It seems that it would just create a Named Range. He does say to set the permissions manually first. But I can't figure what exactly he meant.
Anyways, I was hoping that someone had found a way by now to do this until Google syncs GAS with its counterpart.
My wish is to, through 100% code, select a range in a sheet, within a spreadsheet, and make it so that when I share this whole spreadsheet to a person, that he or she can't edit that range. There will be other parts in that sheet that they have to be able to edit. Just not that range. It is easy to do this manually, but when having to create 100s of spreadsheets, it would be help to be able to do this through GAS.
Thanks.
Part of your question asked about protecting a sheet. Please have a look here: setProtected(protection)
As for programmatically protecting a range no. However, you could protect a sheet, does not need to be in the same spreadsheet and then create an onEdit trigger that would replace any change in your "protected" range with the original source data.
Something like this:
function onLoad() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var protected = ss.getSheets()[1].getRange("A1:B2").getValues(); //You could use openByID to get from a different ss
var target = ss.getSheets()[0].getRange("A1:B2").setValues(protected);
}
function onEdit(){
onLoad();
}
Every time a change is made to the spreadsheet the script will rewrite the data in the sheet for the range you specify.
The easiest approach I've found is to use Data Validation: that is, write a script which will examine each cell to be 'protected' and create and apply a validation rule which enforces entry of the existing content and rejects anything else. [Of course, this also implies that you have designed your spreadsheet such that all entered data is on sheet or sheets separate from those which have formula embedded. On these you use normal sheet protection.]
According to Control protected ranges and sheets in Google Sheets with Apps Script, posted on February 19, 2015, now is possible to protect a Google Sheets range.
From https://developers.google.com/apps-script/reference/spreadsheet/protection
Class Protection
Access and modify protected ranges and sheets. A protected range can
protect either a static range of cells or a named range. A protected
sheet may include unprotected regions. For spreadsheets created with
the older version of Google Sheets, use the PageProtection class
instead.
// Protect range A1:B10, then remove all other users from the list of editors.
var ss = SpreadsheetApp.getActive();
var range = ss.getRange('A1:B10');
var protection = range.protect().setDescription('Sample protected range');
// Ensure the current user is an editor before removing others. Otherwise, if the user's edit
// permission comes from a group, the script will throw an exception upon removing the group.
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
If you are sending 100s of copies of the same sheet. Then create a template sheet, protect the ranges in it manually and send a copy of the template. It will retain the protection.
Sorry but as others have said there is not script method of setting protection at a sub-sheet level.