filed
Job queue using FUSE
git clone git://mccd.space/filed
| Log | Files | Refs | README | LICENSE |
commit 154a0a45320da2d87d2599a2c92e121bb49eb707 parent ebd1b1342664e2d3f08d47a07558c6b479282af0 Author: Marc Coquand <marc@coquand.email> Date: Mon, 15 Dec 2025 12:52:34 +0100 Access right support Diffstat:
| M | README.md | | | 2 | +- |
| M | config.go | | | 46 | ++++++++++++++++++++++++++++++++++++++++++---- |
| M | main.go | | | 101 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- |
| M | newid.go | | | 37 | ++++++++++++++++++++++++++++++++++--- |
| M | qj.1.scd | | | 8 | +++++++- |
| M | setfattr.go | | | 4 | +--- |
| M | store/filemeta.go | | | 6 | +++--- |
7 files changed, 170 insertions(+), 34 deletions(-)
diff --git a/README.md b/README.md
@@ -71,7 +71,7 @@ $ cat /tmp/qj-jobs/active/1
## TODO
-- [ ] Support chmod and chown
+- [x] Support chmod and chown
- [ ] State is configured via environment variable
- [ ] Customizable backoff and timeout before retries
- [ ] "Landlock"-mode, or sandboxed jobs
diff --git a/config.go b/config.go
@@ -18,11 +18,22 @@ type ConfigFile struct {
inode uint64
}
-func (f *ConfigFile) Attr(ctx context.Context, a *fuse.Attr) error {
+func (f ConfigFile) Attr(ctx context.Context, a *fuse.Attr) error {
+ fileMeta, err := f.manager.store.GetFileMeta(f.inode)
+ if err != nil {
+ slog.Error("Could not retrieve file metadata", "error", err)
+ return syscall.EIO
+ } else if fileMeta != nil {
+ a.Mode = os.FileMode(fileMeta.Mode)
+ a.Gid = fileMeta.GID
+ a.Uid = fileMeta.UID
+ } else {
+ a.Mode = 0o440
+ a.Gid = uint32(os.Getgid())
+ a.Uid = uint32(os.Getuid())
+ }
+
config := f.manager.store.GetConfig()
- a.Mode = 0o740
- a.Gid = uint32(os.Getgid())
- a.Uid = uint32(os.Getuid())
a.Inode = f.inode
confJson, err := json.Marshal(config)
if err != nil {
@@ -33,6 +44,33 @@ func (f *ConfigFile) Attr(ctx context.Context, a *fuse.Attr) error {
a.Size = uint64(len(confJson))
return nil
}
+func (f *ConfigFile) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error {
+ slog.Warn("FUSE: Changing access permissions")
+ defaultMode, err := f.manager.store.GetFileMeta(f.inode)
+ if err != nil {
+ return err
+ }
+ if defaultMode == nil {
+ defaultMode = &store.FileMeta{
+ Inode: f.inode,
+ Mode: uint32(0o640),
+ UID: uint32(os.Getgid()),
+ GID: uint32(os.Getuid()),
+ }
+
+ }
+ config := f.manager.store.GetConfig()
+ confJson, err := json.Marshal(config)
+ if err != nil {
+ slog.Error("FUSE: Could not marshal conf", "error", err)
+ return nil
+ }
+
+ resp.Attr.Size = uint64(len(confJson))
+
+ return Setattr(f.manager.store, defaultMode, ctx, req, resp)
+}
+
func (f *ConfigFile) ReadAll(ctx context.Context) (jsonConf []byte, err error) {
slog.Info("FUSE: Read file content")
config := f.manager.store.GetConfig()
diff --git a/main.go b/main.go
@@ -51,7 +51,6 @@ func main() {
c, err := fuse.Mount(
mountpoint,
fuse.FSName("qj"),
- fuse.Subtype("qjfs"),
fuse.AllowOther(),
fuse.DefaultPermissions(),
)
@@ -90,10 +89,10 @@ func (RootDir) Attr(ctx context.Context, a *fuse.Attr) error {
}
func (rd RootDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
- slog.Info("FUSE: Lookup", "name", name)
+ slog.Debug("FUSE: Lookup", "name", name)
switch name {
case store.StatePending:
- return PendingDir{manager: rd.manager}, nil
+ return PendingDir{manager: rd.manager, inode: 2}, nil
case store.StateCompleted:
return JobDir{state: name, manager: rd.manager, inode: 3}, nil
case store.StateFailed:
@@ -124,18 +123,51 @@ func (RootDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
type PendingDir struct {
manager *JobManager
+ inode uint64
}
-func (PendingDir) Attr(ctx context.Context, a *fuse.Attr) error {
- a.Mode = os.ModeDir | 0o750
- a.Gid = uint32(os.Getgid())
- a.Uid = uint32(os.Getuid())
- a.Inode = 2
+func (pd PendingDir) Attr(ctx context.Context, a *fuse.Attr) error {
+ fileMeta, err := pd.manager.store.GetFileMeta(pd.inode)
+ if err != nil {
+ slog.Error("Could not retrieve file metadata", "error", err)
+ return syscall.EIO
+ } else if fileMeta != nil {
+ a.Mode = os.FileMode(fileMeta.Mode)
+ a.Gid = fileMeta.GID
+ a.Uid = fileMeta.UID
+ a.Inode = pd.inode
+
+ } else {
+ a.Mode = os.ModeDir | 0o750
+ a.Gid = uint32(os.Getgid())
+ a.Uid = uint32(os.Getuid())
+ a.Inode = pd.inode
+ }
+ return nil
+}
+
+func (pd *PendingDir) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error {
+ slog.Warn("FUSE: Changing access permissions")
+ defaultMode, err := pd.manager.store.GetFileMeta(pd.inode)
+ if defaultMode == nil {
+ defaultMode = &store.FileMeta{
+ Inode: pd.inode,
+ Mode: uint32(os.ModeDir | 0o750),
+ UID: uint32(os.Getgid()),
+ GID: uint32(os.Getuid()),
+ }
+
+ }
+ err = Setattr(pd.manager.store, defaultMode, ctx, req, resp)
+ if err != nil {
+ return err
+ }
+
return nil
}
-func (jd PendingDir) ReadDirAll(ctx context.Context) (entries []fuse.Dirent, err error) {
- jobs, err := jd.manager.store.ListJobsByState(store.StatePending)
+func (pd PendingDir) ReadDirAll(ctx context.Context) (entries []fuse.Dirent, err error) {
+ jobs, err := pd.manager.store.ListJobsByState(store.StatePending)
if err != nil {
slog.Error("FUSE: Could not find jobs", "error", err)
return entries, nil
@@ -145,27 +177,27 @@ func (jd PendingDir) ReadDirAll(ctx context.Context) (entries []fuse.Dirent, err
}
return entries, nil
}
-func (d PendingDir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) {
+func (pd PendingDir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) {
slog.Info("Creating job file", "name", req.Name)
f := &JobCreationFile{
id: req.Name,
- store: d.manager.store,
+ store: pd.manager.store,
uid: req.Uid,
gid: req.Gid,
mode: req.Mode,
}
return f, f, nil
}
-func (jd PendingDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
+func (pd PendingDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
slog.Debug("FUSE: Lookup", "name", name)
- job, err := jd.manager.store.GetJob(name)
+ job, err := pd.manager.store.GetJob(name)
if err != nil {
return nil, syscall.ENOENT
}
if job.State == store.StatePending {
slog.Debug("FUSE: Found job", "id", job.ID)
- return &File{job, jd.manager}, nil
+ return &File{job, pd.manager}, nil
} else {
return nil, syscall.ENOENT
}
@@ -264,11 +296,42 @@ type JobDir struct {
}
func (jd JobDir) Attr(ctx context.Context, a *fuse.Attr) error {
- a.Mode = os.ModeDir | 0o750
+ fileMeta, err := jd.manager.store.GetFileMeta(jd.inode)
+ if err != nil {
+ slog.Error("Could not retrieve file metadata", "error", err)
+ return syscall.EIO
+ } else if fileMeta != nil {
+ a.Mode = os.FileMode(fileMeta.Mode)
+ a.Gid = fileMeta.GID
+ a.Uid = fileMeta.UID
+ a.Inode = jd.inode
+
+ } else {
+ a.Mode = os.ModeDir | 0o750
+ a.Gid = uint32(os.Getgid())
+ a.Uid = uint32(os.Getuid())
+ a.Inode = jd.inode
+ }
+ return nil
+}
+
+func (jd JobDir) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error {
+ slog.Warn("FUSE: Changing access permissions")
+ defaultMode, err := jd.manager.store.GetFileMeta(jd.inode)
+ if defaultMode == nil {
+ defaultMode = &store.FileMeta{
+ Inode: jd.inode,
+ Mode: uint32(os.ModeDir | 0o750),
+ UID: uint32(os.Getgid()),
+ GID: uint32(os.Getuid()),
+ }
+
+ }
+ err = Setattr(jd.manager.store, defaultMode, ctx, req, resp)
+ if err != nil {
+ return err
+ }
- a.Gid = uint32(os.Getgid())
- a.Uid = uint32(os.Getuid())
- a.Inode = jd.inode
return nil
}
diff --git a/newid.go b/newid.go
@@ -6,6 +6,7 @@ import (
"math/rand"
"os"
"qj/store"
+ "syscall"
"time"
"bazil.org/fuse"
@@ -36,13 +37,43 @@ type NewIdFile struct {
}
func (f NewIdFile) Attr(ctx context.Context, a *fuse.Attr) error {
- a.Mode = 0o440
- a.Gid = uint32(os.Getgid())
- a.Uid = uint32(os.Getuid())
+ fileMeta, err := f.manager.store.GetFileMeta(f.inode)
+ if err != nil {
+ slog.Error("Could not retrieve file metadata", "error", err)
+ return syscall.EIO
+ } else if fileMeta != nil {
+ a.Mode = os.FileMode(fileMeta.Mode)
+ a.Gid = fileMeta.GID
+ a.Uid = fileMeta.UID
+ } else {
+ a.Mode = 0o440
+ a.Gid = uint32(os.Getgid())
+ a.Uid = uint32(os.Getuid())
+ }
a.Inode = f.inode
a.Size = uint64(4)
+
return nil
}
+func (f *NewIdFile) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error {
+ slog.Warn("FUSE: Changing access permissions")
+ defaultMode, err := f.manager.store.GetFileMeta(f.inode)
+ if err != nil {
+ return err
+ }
+ if defaultMode == nil {
+ defaultMode = &store.FileMeta{
+ Inode: f.inode,
+ Mode: uint32(0o440),
+ UID: uint32(os.Getgid()),
+ GID: uint32(os.Getuid()),
+ }
+
+ }
+ resp.Attr.Size = uint64(4)
+ return Setattr(f.manager.store, defaultMode, ctx, req, resp)
+}
+
func (f *NewIdFile) ReadAll(ctx context.Context) ([]byte, error) {
slog.Info("FUSE: Read file content")
return randomJobId(f.manager.store, 4), nil
diff --git a/qj.1.scd b/qj.1.scd
@@ -45,7 +45,13 @@ By default, all write operations are allowed by the executing user, and all
read operations by the executing user's group. All other users have zero
access to the system.
-(TODO) The access rights can be modified using _CHOWN(1)_ and _CHMOD(1)_.
+It is recommended to that the admin updates the users and group to apply
+principle of least access. Importantly, the system is intended for only
+trusted scripts: the job user has access to the state, and is thus able to
+rewrite access rights. However, users can use _namespaces(7)_ or _Landlock(7)_
+to limit access rights of the running script.
+
+Access rights can be modified using _CHOWN(1)_ and _CHMOD(1)_.
# EXAMPLES
diff --git a/setfattr.go b/setfattr.go
@@ -10,7 +10,7 @@ import (
"bazil.org/fuse"
)
-func Setattr(store *store.Store, meta *store.FileMeta, content []byte, inode uint64, ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error {
+func Setattr(store *store.Store, meta *store.FileMeta, ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error {
if req.Valid.Mode() {
meta.Mode = uint32(req.Mode)
}
@@ -32,7 +32,5 @@ func Setattr(store *store.Store, meta *store.FileMeta, content []byte, inode uin
resp.Attr.Mode = os.FileMode(meta.Mode)
resp.Attr.Uid = meta.UID
resp.Attr.Gid = meta.GID
- resp.Attr.Size = uint64(len(content))
-
return nil
}
diff --git a/store/filemeta.go b/store/filemeta.go
@@ -12,14 +12,14 @@ type FileMeta struct {
GID uint32
}
-// Returns file metadata, if it exists.
-func (st *Store) GetFileMeta(inode uint64) (m *FileMeta, err error) {
+func (st *Store) GetFileMeta(inode uint64) (*FileMeta, error) {
query := `
SELECT inode, mode, uid, gid
FROM file_meta
WHERE inode = ?
`
- err = st.db.QueryRow(query, inode).Scan(&m.Inode, &m.Mode, &m.UID, &m.GID)
+ m := &FileMeta{}
+ err := st.db.QueryRow(query, inode).Scan(&m.Inode, &m.Mode, &m.UID, &m.GID)
if err == sql.ErrNoRows {
// No metadata exists, that's ok
return nil, nil