21

I have a script that does a number of different things, most of which do not require any special privileges. However, one specific section, which I have contained within a function, needs root privileges.

I don't wish to require the entire script to run as root, and I want to be able to call this function, with root privileges, from within the script. Prompting for a password if necessary isn't an issue since it is mostly interactive anyway. However, when I try to use sudo functionx, I get:

sudo: functionx: command not found

As I expected, export didn't make a difference. I'd like to be able to execute the function directly in the script rather than breaking it out and executing it as a separate script for a number of reasons.

Is there some way I can make my function "visible" to sudo without extracting it, finding the appropriate directory, and then executing it as a stand-alone script?

The function is about a page long itself and contains multiple strings, some double-quoted and some single-quoted. It is also dependent upon a menu function defined elsewhere in the main script.

I would only expect someone with sudo ANY to be able to run the function, as one of the things it does is change passwords.

  • The fact that there are several functions involved makes it even more complicated and prone to failure. You now have to find all such dependencies (and all their dependencies too, if any...to however many levels deep) including any other functions that the menu function might call and declare them too. – cas Mar 11 '16 at 6:06
  • Agreed, and I may have to just bite the bullet and break it up (and do my best to accurately determine the path it was run from, plus hope the end user keeps the files together) if there are no better alternatives. – BryKKan Mar 11 '16 at 6:33
19

I will admit that there's no simple, intuitive way to do this, and this is a bit hackey. But, you can do it like this:

function hello()
{
    echo "Hello!"
}

# Test that it works.
hello

FUNC=$(declare -f hello)
sudo bash -c "$FUNC; hello"

Or more simply:

sudo bash -c "$(declare -f hello); hello"

It works for me:

$ bash --version
GNU bash, version 4.3.42(1)-release (x86_64-apple-darwin14.5.0)
$ hello
Hello!
$
$ FUNC=$(declare -f hello)
$ sudo bash -c "$FUNC; hello"
Hello!

Basically, declare -f will return the contents of the function, which you then pass to bash -c inline.

If you want to export all functions from the outer instance of bash, change FUNC=$(declare -f hello) to FUNC=$(declare -f).

Edit

To address the comments about quoting, see this example:

$ hello()
> {
> echo "This 'is a' test."
> }
$ declare -f hello
hello ()
{
    echo "This 'is a' test."
}
$ FUNC=$(declare -f hello)
$ sudo bash -c "$FUNC; hello"
Password:
This 'is a' test.
  • 1
    This only works by accident, because echo "Hello!" is effectively the same as echo Hello! (i.e. double-quotes make no difference for this particular echo command). In many/most other circumstances, the double-quotes in the function are likely to break the bash -c command. – cas Mar 11 '16 at 5:21
  • 1
    This does answer the original question, so if I don't get a better solution I'll accept it. However, it does break my particular function (see my edit) since it's dependent on functions defined elsewhere in the script. – BryKKan Mar 11 '16 at 6:20
  • 1
    i did some testing earlier this afternoon (using bash -xc rather than just bash -c) and it looks like bash is smart enough to re-quote things in this situation, even to the extent of replacing double-quotes with single quotes and changing ' to '\'' if necessary. I'm sure there will be some cases it can't handle, but it definitely works for at least simple and moderately complex cases - e.g. try function hello() { filename="this is a 'filename' with single quotes and spaces" ; echo "$filename" ; } ; FUNC=$(declare -f hello) ; bash -xc "$FUNC ; hello" – cas Mar 11 '16 at 12:07
  • 4
    @cas declare -f prints out the function definition in a way that can be re-parsed by bash, so bash -c "$(declare -f)" does work correctly (assuming that the outer shell is also bash). The example you posted shows it working correctly — where the quotes were changed is in the trace, because bash prints out traces in shell syntax, e.g. try bash -xc 'echo "hello world"' – Gilles 'SO- stop being evil' Mar 11 '16 at 19:57
  • 3
    Excellent answer. I implemented your solution - I would note that you can import the script itself from within the script, provided you nest it within an conditional which checks if against sudo yourFunction being found (otherwise you get a segmentation error from the recursion) – GrayedFox Feb 10 '17 at 12:38
5

The "problem" is that sudo clears the environment (except for a handful of allowed variables) and sets some variables to pre-defined safe values in order to protect against security risks. in other words, this is not actually a problem. It's a feature.

For example, if you set PATH="/path/to/myevildirectory:$PATH" and sudo didn't set PATH to a pre-defined value then any script that didn't specify the full pathname to ALL commands it runs (i.e. most scripts) would look in /path/to/myevildirectory before any other directory. Put commands like ls or grep or other common tools in there and you can easily do whatever you like on the system.

The easiest / best way is to re-write the function as a script and save it somewhere in the path (or specify the full path to the script on the sudo command line - which you'll need to do anyway unless sudo is configured to allow you to run ANY command as root), and make it executable with chmod +x /path/to/scriptname.sh

Rewriting a shell function as a script is as simple as just saving the commands inside the function definition to a file (without the function ..., { and } lines).

  • This does not answer the question in any way. He specifically wants to avoid putting it in a script. – Will Mar 11 '16 at 5:37
  • 1
    Also sudo -E avoids clearing the environment. – Will Mar 11 '16 at 5:37
  • I understand to some extent why it is happening. I was hoping there was some means to temporarily override this behavior. Somewhere else a -E option was mentioned, though that didn't work in this case. Unfortunately, while I appreciate the explanation of how to make it a standalone script, that specifically doesn't answer the question, because I wanted a means to avoid that. I have no control over where the end user places the script and I'd like to avoid both hard-coded directories and the song and dance of trying to accurately determine where the main script was run from. – BryKKan Mar 11 '16 at 5:41
  • it doesn't matter whether that's what the OP asked for or not. If what he wants either won't work or can only be made to work by doing something extremely insecure then they need to be told that and provided with an alternative - even if the alternative is something they explicitly stated they don't want (because sometimes that's the only or the best way to do it safely). It would be irresponsible to tell someone how to shoot themselves in the foot without giving them warning about the likely consequences of pointing a gun at their feet and pulling the trigger. – cas Mar 11 '16 at 5:43
  • 1
    @cas That is true. It can't be done securely is an acceptable answer in some circumstances. See my last edit though. I'd be curious to know if your opinion on the security implications is the same given that. – BryKKan Mar 11 '16 at 6:30
3

I've written my own Sudo bash function to do that, it works to call functions and aliases :

function Sudo {
        local firstArg=$1
        if [ $(type -t $firstArg) = function ]
        then
                shift && command sudo bash -c "$(declare -f $firstArg);$firstArg $*"
        elif [ $(type -t $firstArg) = alias ]
        then
                alias sudo='\sudo '
                eval "sudo $@"
        else
                command sudo "$@"
        fi
}
2

You can combine functions and aliases

Example:

function hello_fn() {
    echo "Hello!" 
}

alias hello='bash -c "$(declare -f hello_fn); hello_fn"' 
alias sudo='sudo '

then sudo hello works

1

Here's a variation on Will's answer. It involves an additional cat process, but offers the comfort of heredoc. In a nutshell it goes like this:

f () 
{
    echo ok;
}

cat <<EOS | sudo bash
$(declare -f f)
f
EOS

If you want more food for thought, try this:

#!/bin/bash

f () 
{ 
    x="a b"; 
    menu "$x"; 
    y="difficult thing"; 
    echo "a $y to parse"; 
}

menu () 
{
    [ "$1" == "a b" ] && 
    echo "here's the menu"; 
}

cat <<EOS | sudo bash
$(declare -f f)
$(declare -f menu)
f
EOS

The output is:

here's the menu
a difficult thing to pass

Here we've got the menu function corresponding with the one in the question, which is "defined elsewhere in the main script". If the "elsewhere" means its definition has already been read at this stage when the function demanding sudo is being executed, then the situation is analogous. But it might not have been read yet. There may be another function that will yet trigger its definition. In this case declare -f menu has to be replaced with something more sophisticated, or the whole script corrected in a way that the menu function is already declared.

  • Very interesting. I'll have to try it out at some point. And yes, the menu function would have been declared before this point, as fis invoked from a menu. – BryKKan Jan 25 '18 at 23:54
0

Assuming that your script is either (a) self-contained or (b) can source its components based on its location (rather than remembering where your home directory is), you could do something like this:

  • use the $0 pathname for the script, using that in the sudo command, and pass along an option which the script will check, to call the password updating. As long as you rely on finding the script in the path (rather than just run ./myscript), you should get an absolute pathname in $0.
  • since the sudo runs the script, it has access to the functions it needs in the script.
  • at the top of the script (rather, past the function declarations), the script would check its uid and realize that it was run as the root user, and seeing that it has the option set to tell it to update the password, go and do that.

Scripts can recur on themselves for various reasons: changing privileges is one of those.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.