`touch` a file that uses ACL on Linux causes "Operation not permitted" - java

In Java code I want to "touch" a file. I want to update the timestamps to the current time. The file uses ACL. And this seems to be the problem.
The file:
$ ll file.xml
-rw-rwxrw-+ 1 root root 8611 Oct 4 17:28 file.xml
$ getfacl file.xml
# file: file.xml
# owner: root
# group: root
user::rw-
user:tomcat8:rwx
group::r-x
mask::rwx
other::rw-
And my Java app runs from Tomcat 8 with user tomcat8. A sudo -u tomcat8 touch file.xml works. It also works if I completely remove ACL and set tomcat8 as owner. But this is not possible in the production environment.
So at first I tried Apache common-io:
FileUtils.touch(path);
This causes an IOException. I debugged it a bit more and found out that the library calls FileSystem.setLastModifiedTime which calls the Linux function utimes.
I debugged the Linux touch command and saw it calls another more modern function: utimensat(0, NULL, NULL, 0). It also calls dup2and duplicates the file descriptor.
So I built my own touch method in Java:
long time = System.currentTimeMillis();
FileTime fileTimeNow = FileTime.fromMillis(time);
BasicFileAttributeView fileAttributeView = Files.getFileAttributeView(derivative.toPath(), BasicFileAttributeView.class);
fileAttributeView.setTimes(fileTimeNow, fileTimeNow, fileTimeNow);
This throws an Exception too (Operation not permitted).
Internally it calls utimensat(69, NULL, [{1538666780, 483000000}, {1538666780, 483000000}], 0).
I can not set null on .setTimes(...). This call gets ignored. And there is no Java-way to duplicate a file descriptor (dup2). So I can not test further steps to make it more like Linux' touch.
How to make this working when a file uses ACL? I don't want to run external programs (touch).

If the file is not written concurrently, you can open it, read its first byte, and write it back again at offset zero. This will update the modification time of the file without requiring ownership permissions.
(By the way, the ACL looks really curious, particularly the other:: rw- part.)

Here's man utimensat:
Permissions requirements
To set both file timestamps to the current time (i.e., times is NULL, or both tv_nsec fields specify UTIME_NOW), either:
the caller must have write access to the file;
the caller's effective user ID must match the owner of the file; or
the caller must have appropriate privileges.
To make any change other than setting both timestamps to the current time (i.e., times is not NULL, and neither tv_nsec field is UTIME_NOW and neither tv_nsec field is UTIME_OMIT), either condition 2 or 3 above must
apply.
You have #1, but not #2 or #3. If you ask touch to explicitly set the time to the current timestamp, it fails as well:
$ getfacl test | sed -e "s/$USER/myuser/"
# file: test
# owner: root
# group: root
user::rw-
user:myuser:rwx
group::r--
mask::rwx
other::r--
$ touch -d "#$(date +%s)" test
touch: setting times of ‘test’: Operation not permitted
I don't have any good suggestions for what to do instead though. You could either make a no-op change to the file, or call touch as an external command:
String path="some path";
// See https://stackoverflow.com/a/52651585 for why we're not doing this via Java
int result = Runtime.getRuntime().exec(new String[] { "touch", "--", path }).waitFor();
if(result != 0) throw new IOException("Can't update timestamp");

Related

Java/JavaFX ProcessHandle possibly not finding all processes (Linux/Debian)

I've a JavaFX application where I've a list of a bunch of script files. Once the application loads, it reads it and and checks which ones are running.
To do that I use a ProcessHandle, as mentioned in various examples here on StackOverflow and other guides/tutorials on the internet.
The problem is, it never finds any of them. There for I programmatically started one, which I know for a fact that it will be running, via Process process = new ProcessBuilder("/path/to/file/my_script.sh").start(); - and it won't find this one either.
Contents of my_script.sh:
#!/bin/bash
echo "Wait for 5 seconds"
sleep 5
echo "Completed"
Java code:
// List of PIDs which correspond to the processes shown after "INFO COMMAND:"
System.out.println("ALL PROCESSES: " + ProcessHandle.allProcesses().toList());
Optional<ProcessHandle> scriptProcessHandle = ProcessHandle.allProcesses().filter(processHandle -> {
System.out.println("INFO COMMAND: " + processHandle.info().command());
Optional<String> processOptional = processHandle.info().command();
return processOptional.isPresent() && processOptional.get().equals("my_script.sh");
}).findFirst();
System.out.println("Script process handle is present: " + scriptProcessHandle.isPresent());
if (scriptProcessHandle.isPresent()) { // Always false
// Do stuff
}
Thanks to the good old fashioned System.out.println(), I noticed that I get this in my output console every time:
ALL PROCESSES: [1, 2, 28, 85, 128, 6944, 21174, 29029, 29071]
INFO COMMAND: Optional[/usr/bin/bwrap]
INFO COMMAND: Optional[/usr/bin/bash]
INFO COMMAND: Optional[/app/idea-IC/jbr/bin/java]
INFO COMMAND: Optional[/app/idea-IC/bin/fsnotifier]
INFO COMMAND: Optional[/home/username/.jdks/openjdk-17.0.2/bin/java]
INFO COMMAND: Optional[/usr/bin/bash]
INFO COMMAND: Optional[/home/username/.jdks/openjdk-17.0.2/bin/java]
INFO COMMAND: Optional[/home/username/.jdks/openjdk-17.0.2/bin/java]
INFO COMMAND: Optional[/usr/bin/bash]
Script process handle is present: false
The first line in the Javadoc of ProcessHandle.allProcess() reads:
Returns a snapshot of all processes visible to the current process.
So how come I can't see the rest of the operating system's processes?
I'm looking for a non-os-dependent solution, if possible. Why? For better portability and hopefully less maintenance in the future.
Notes:
A popular solution for GNU/Linux seems to be to check the proc entries, but I don't know if that would work for at least the majority of the most popular distributions - if it doesn't, adding support for them in a different way, would create more testing and maintenance workload.
I'm aware of ps, windir, tasklist.exe possible solutions (worst comes to worst).
I found the JavaSysMon library but it seems dead and unfortunately:
CPU speed on Linux only reports correct values for Intel CPUs
Edit 1:
I'm on Pop_OS! and installed IntelliJ via the PopShop as flatpak.
In order to start it as root as suggested by mr mcwolf, I went to /home/username/.local/share/flatpak/app/com.jetbrains.IntelliJ-IDEA-Community/x86_64/stable/active/export/bin and found com.jetbrains.IntelliJ-IDEA-Community file.
When I run sudo ./com.jetbrains.IntelliJ-IDEA-Community or sudo /usr/bin/flatpak run --branch=stable --arch=x86_64 com.jetbrains.IntelliJ-IDEA-Community in my terminal, I get error: app/com.jetbrains.IntelliJ-IDEA-Community/x86_64/stable not installed
So I opened the file and ran its contents:
exec /usr/bin/flatpak run --branch=stable --arch=x86_64 com.jetbrains.IntelliJ-IDEA-Community "$#"
This opens IntelliJ, but not as root, so instead I ran:
exec sudo /usr/bin/flatpak run --branch=stable --arch=x86_64 com.jetbrains.IntelliJ-IDEA-Community "$#"
Which prompts for a password and when I write it in, the terminal crashes.
Edit 1.1:
(╯°□°)╯︵ ┻━┻ "flatpak run" is not intended to be ran with sudo
Edit 2:
As mr mcwolf said, I downloaded the IntelliJ from the official website, extracted it and ran the idea.sh as root.
Now a lot more processes are shown. 1/3 of them show up as INFO COMMAND: Optional.empty.
scriptProcessHandle.isPresent() is still unfortunately returning false. I searched through them and my_script.sh is nowhere to be found. I also tried processOptional.isPresent() && processOptional.get().equals("/absolute/path/to/my_script.sh") but I still get false on isPresent() and it's not in the list of shown processes.
Though the last sentence might be a different problem. I'll do more digging.
Edit 3:
Combining .commandLine() and .contains() (instead of .equals()) solves the problem mentioned in "Edit 2".
Optional<ProcessHandle> scriptProcessHandle = ProcessHandle.allProcesses().filter(processHandle -> {
System.out.println("INFO COMMAND LINE: " + processHandle.info().commandLine());
Optional<String> processOptional = processHandle.info().commandLine();
return processOptional.isPresent() && processOptional.get().contains("/absolute/path/to/my_script.sh");
}).findFirst();
System.out.println("Script process handle is present: " + scriptProcessHandle.isPresent());
if (scriptProcessHandle.isPresent()) { // Returns true
// Do stuff
}
.commandLine() also shows script arguments, so that must be kept in mind.

How can I access calendars on my Mac with Java?

I want to read data stored locally by the Apple Calendar app on my Mac (12.1 Monterey).
The data is stored in subdirectories of ~/Library/Calendars/ with one subdirectory per calendar.
The problem: When I try to get a list of files from there, Java returns null:
String userHomeDir = System.getProperty("user.home");
File calendarRoot = new File(userHomeDir + "/Library/Calendars/");
File[] calendars = calendarRoot.listFiles();
System.out.println("Number of files: " + calendars.length); // NPE thrown here
File permissions are as follows:
~/Library: drwx------+ (owner: my user)
~/Library/Calendars: drwxr-xr-x# (owner: my user)
Listing files in Library works fine.
How can I access that folder?
Short answer
Give it up. Apple has made it next to impossible to elegantly get a Java app to read calendar data.
Long answer
Since some versions (Catalina?) the directory ~/Library/Calendars/ and all subdirectories (and files therein) are protected by MacOS using extended attributes, namely com.apple.quarantine.
It used to be possible to grant applications the specific right to access calendar data using System Settings - Security and Privacy - Privacy - Calendar. However, the manual +-Button has gone now.
What I will do is use some zsh script to export the desired calendar events to another directory and remove the com.apple.quarantine attribute from there, too.
This is not elegant and leaves the Java world, but for my case, having a Java command line application being started from a designated shell script, it works rather nicely.
Here's what I came up with:
#!/bin/zsh
calendars="/Users/yourUserName/Library/Calendars"
target="/Users/yourUserName/some/other/directory/Calendar_Export"
cd ${calendars}
calsource=""
for f in *.calendar
do
linesFound=`grep -c '<string>Your Calendar Name</string>' ${f}/Info.plist`
if [[ ${linesFound} -eq 1 ]]
then
echo "The relevant calendar resides at " ${f}", copying all events"
calsource=${calendars}/${f}/Events
fi
done
if [[ ${calsource} != "" ]]
then
rm ${target}/*
cp ${calsource}/* ${target}/
xattr -d com.apple.quarantine ${target}/*
fi

Fail to seteuid in java by calling JNI

My app need to run as none-root user but need to switch to other user to execute some commands.
I tried to:
write a JNI,
JNIEXPORT void JNICALL Java_SetUIDJNI_setuid(JNIEnv *env, jobject thisObj,jstring uname) {
const char *name = jstringTostring(env,uname);
int pid;
struct passwd *p;
show_ids();
if ((p = getpwnam(name)) == NULL) {
perror(name);
exit(EXIT_FAILURE);
}
pid = (int) p->pw_uid;
printf("pid=%d\n",pid);
if (seteuid (pid) < 0) {
perror ("setuid");
exit (EXIT_FAILURE);
}
show_ids();
}
build it as root and chmod u+s
-rwsr-xr-x 1 root root 76155 Aug 7 16:56 libsetuid.so*
call it in java
api = new SetUIDJNI(); // invoke the native method
api.setuid("slurm");
But if run it as none-root, it does not work
/opt/jdk1.8/jre/bin/java -Djava.library.path=../jni HelloJNI
The real user ID is: 1000
The effective user ID is :1000 <= which expected is 0
setuid: Operation not permitted
But it works if runner is root
The real user ID is: 0
The effective user ID is :0
pid=1002The real user ID is: 0
The effective user ID is :1002
Anything wrong here?
UPDATE
Modify the JNI part to executable c
void show_ids (void)
{
printf ("The real user ID is: %d\n", getuid());
printf ("The effective user ID is :%d\n", geteuid());
}
int main(void)
{
show_ids();
if (seteuid (1002) < 0) {
perror ("setuid");
exit (EXIT_FAILURE);
}
show_ids();
return (0);
}
Build it as root and run chmod u+s
-rwsr-xr-x 1 root root 8814 Aug 9 11:44 a.out*
Run it as normal user and works
./a.out
The real user ID is: 1000
The effective user ID is :0
The real user ID is: 1000
The effective user ID is :1002
The reason that seteuid was not working for you from JNI was that the effective user id for the JVM process was not 0 (root).
You were apparently attempting to make the effective user id by setting the "setuid" bit on your native library. That won't work. Similarly, making the JAR file (or a class file) as setuid won't work. The setuid bit is only meaningful on a executable file; i.e. a file that the OS itself knows how to execute.
So how do we implement "setuid root behavior" for a Java program?
In theory, you could mark the /usr/bin/java as setuid to root. Don't do that! If you do that, every Java program you run will be run as root. That would be bad.
In theory, you could write a shell script to launch your application; e.g.
#!/bin/sh
java some.pkg.Main "$#"
and mark the script as setuid to root. Don't do that! Setuid shell scripts are a security risk. (On some versions of Linux / UNIX the setuid bit is not respected for shell scripts anyway.)
The solution is to write a custom JVM launcher in native code, compile and link it, and make the launcher executable setuid. Note that the launcher must be written very carefully to project against someone subverting it. For example:
It should ignore CLASSPATH environment variable, and should not allow the classpath to be supplied any other way.
It should not take a user-supplied JAR file as a parameter.
It should take care that the user can't trick it into running the wrong Java code by interfering with the path to the JAR file.
And other things ... that I haven't thought of!
In fact, it is probably safer to run the Java application as a privileged user ... and not rely on "setuid root" at all.

Ansible: Changing permission of a directory issue

I'm running the following Ansible task to change permission of a directory and its content.
- name: Change ownership of everything below /opt/as2/app-server
file: path=/opt/as2/app-server state=directory recurse=yes owner=adrt group=adrt
When running it I get the following issue:
TASK [appserver : Change ownership of everything below /opt/as2/app-server] ****
fatal: [192.168.1.182]: FAILED! => {"changed": false, "failed": true, "module_stderr": "", "module_stdout": "Traceback (most recent call last):\r\n File \"/tmp/ansible_UrBo6x/ansible_module_file.py\", line 451, in \r\n main()\r\n File \"/tmp/ansible_UrBo6x/ansible_module_file.py\", line 335, in main\r\n changed |= recursive_set_attributes(module, to_bytes(file_args['path'], errors='surrogate_or_strict'), follow, file_args)\r\n File \"/tmp/ansible_UrBo6x/ansible_module_file.py\", line 146, in recursive_set_attributes\r\n changed |= module.set_fs_attributes_if_different(tmp_file_args, changed)\r\n File \"/tmp/ansible_UrBo6x/ansible_modlib.zip/ansible/module_utils/basic.py\", line 1163, in set_fs_attributes_if_different\r\n File \"/tmp/ansible_UrBo6x/ansible_modlib.zip/ansible/module_utils/basic.py\", line 929, in set_owner_if_different\r\n File \"/tmp/ansible_UrBo6x/ansible_modlib.zip/ansible/module_utils/basic.py\", line 842, in user_and_group\r\nOSError: [Errno 2] No such file or directory: '/opt/as2/app-server-1.0.0/apps/station/WEB-INF/classes/org/adroitlogic/isuite/metrics/As2MetricsService/usr/bin/python$tt__collectStats_closure14.class'\r\n", "msg": "MODULE FAILURE"}
Basically it says there is no such file or directory as,
/opt/as2/app-server-1.0.0/apps/station/WEB-INF/classes/org/adroitlogic/isuite/metrics/As2MetricsService/usr/bin/python$tt__collectStats_closure14.class
The content of the directory, /opt/as2/app-server/apps/station/WEB-INF/classes/org/adroitlogic/isuite/metrics/ is,
As2MetricsService$_$tt__CountStatisticsLists_closure3.class
As2MetricsService$_$tt__collectStats_closure10.class
As2MetricsService$_$tt__collectStats_closure11.class
As2MetricsService$_$tt__collectStats_closure12.class
As2MetricsService$_$tt__collectStats_closure13.class
As2MetricsService$_$tt__collectStats_closure14.class
As2MetricsService$_$tt__collectStats_closure15.class
As2MetricsService$_$tt__collectStats_closure4.class
As2MetricsService$_$tt__collectStats_closure5.class
As2MetricsService$_$tt__collectStats_closure6.class
As2MetricsService$_$tt__collectStats_closure7.class
As2MetricsService$_$tt__collectStats_closure8.class
As2MetricsService$_$tt__collectStats_closure9.class
As2MetricsService$_CountStatisticsLists_closure1.class
As2MetricsService$_collectStats_closure2.class
As2MetricsService.class
There are no subdirectories.
Also when I run the command chown -R adrt:adrt . inside the directory /opt/as2/app-server it executes without any issue.
Help me to understand what is happening here.
Help me to understand what is happening here.
You have just found a bug in Ansible which causes modules to fail when the names of files it processes contain $_ sequence.
The name is passed without escaping the $ character (or rather with an explicit conversion request os.path.expandvars(filename)) and the sequence $_ is processed as an built-in variable resolving to the path of the current process (/usr/bin/python in this case, as Ansible uses Python to run its modules).
In result the file name:
As2MetricsService$_$tt__collectStats_closure14.class
is interpreted as:
As2MetricsService/usr/bin/python$tt__collectStats_closure14.class
and the system throws an error that the file does not exist (which is true).
Until it is fixed, I guess you have to call chown with the command module

odd .bat file behavior

I have a bat file with the following contents:
set logfile= D:\log.txt
java com.stuff.MyClass %1 %2 %3 >> %logfile%
when I run the bat file though, I get the following:
C:\>set logfile= D:\log.txt
C:\>java com.stuff.MyClass <val of %1> <val of %2> <val of %3> 1>>D:\log.txt
The parameter is incorrect.
I'm almost positive the "The parameter is incorrect." is due to the extraneous 1 in there. I also think this might have something with the encoding of the .bat file, but I can't quite figure out what is causing it. Anyone ever run into this before or know what might be causing it and how to fix it?
Edit
And the lesson, as always, is check if its plugged in first before you go asking for help. The bat file, in version control, uses D:\log.txt because it is intended to be run from the server which contains a D drive. When testing my changes and running locally, on my computer which doesn't have a D drive, I failed to make the change to use C:\log.txt which is what caused the error. Sorry for wasting you time, thanks for the help, try to resist the urge to downvote me too much.
I doubt that that's the problem - I expect the command processor to deal with that part for you.
Here's evidence of it working for me:
Test.java:
public class Test
{
public static void main(String args[]) throws Exception
{
System.out.println(args.length);
for (String arg : args)
{
System.out.println(arg);
}
}
}
test.bat:
set logfile= c:\users\jon\test\test.log
java Test %1 %2 %3 >> %logfile%
On the command line:
c:\Users\Jon\Test> [User input] test.bat first second third
c:\Users\Jon\Test>set logfile= c:\users\jon\test\test.log
c:\Users\Jon\Test>java Test first second third 1>>c:\users\jon\test\test.log
c:\Users\Jon\Test> [User input] type test.log
3
first
second
third
the 1 is not extraneous: it is inserted by cmd.exe meaning stdout (instead of ">>", you can also write "1>>". contrast this to redirecting stderr: "2>>"). so the problem must be with your parameters.
This may seem like a stupid question, but is there an existing D: drive in the context that the bat file runs in?
Once I had a case where a bat file was used as the command line of a task within the Task Manager, but the Run As user was set to a local user on the box, giving no access to network drives.
Interpolated for your case, if the D: drive were a network drive, running the bat file as, say, the local administrator account on that machine instead of a domain user account would likely fail to have access to D:.

Categories