added login page and authentication middleware, altought not yet implemented
ci/woodpecker/push/woodpecker Pipeline failed Details

templ
Johannes Bülow 2023-10-03 09:31:16 +02:00
parent f6ab0bb4e2
commit ddc95f49cd
Signed by untrusted user who does not match committer: jmb
GPG Key ID: B56971CF7B8F83A6
39 changed files with 772 additions and 5911 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="IU-232.9559.62">
<component name="dataSourceStorageLocal" created-in="IU-232.9921.47">
<data-source name="filegate@filegate.dev.jmbit.de" uuid="28e7aa7f-2fe3-45ee-997f-c35496a4a3eb">
<database-info product="PostgreSQL" version="15.3 (Debian 15.3-0+deb12u1)" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.6.0" dbms="POSTGRES" exact-version="15.3" exact-driver-version="42.6">
<identifier-quote-string>&quot;</identifier-quote-string>

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/pgsetup.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/sqliteschema.sql" dialect="SQLite" />
</component>
</project>

View File

@ -8,7 +8,45 @@
<option name="autoReloadType" value="ALL" />
</component>
<component name="ChangeListManager">
<list default="true" id="eeedfed2-369e-4948-a46c-18edddaeec9c" name="Changes" comment="" />
<list default="true" id="eeedfed2-369e-4948-a46c-18edddaeec9c" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/Makefile" afterDir="false" />
<change afterPath="$PROJECT_DIR$/database/user.go" afterDir="false" />
<change afterPath="$PROJECT_DIR$/filegate.service" afterDir="false" />
<change afterPath="$PROJECT_DIR$/templates/login.gohtml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/web/auth/hashPassword.go" afterDir="false" />
<change afterPath="$PROJECT_DIR$/web/auth/login.go" afterDir="false" />
<change afterPath="$PROJECT_DIR$/web/auth/middleware.go" afterDir="false" />
<change afterPath="$PROJECT_DIR$/web/routes.go" afterDir="false" />
<change afterPath="$PROJECT_DIR$/web/session.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/dataSources.local.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dataSources.local.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/dataSources/28e7aa7f-2fe3-45ee-997f-c35496a4a3eb.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/sqldialects.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Dockerfile" beforeDir="false" afterPath="$PROJECT_DIR$/Dockerfile" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Installation.md" beforeDir="false" afterPath="$PROJECT_DIR$/Installation.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/config.go" beforeDir="false" afterPath="$PROJECT_DIR$/config/config.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/defaults.go" beforeDir="false" afterPath="$PROJECT_DIR$/config/defaults.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/struct.go" beforeDir="false" afterPath="$PROJECT_DIR$/config/struct.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/database/files.go" beforeDir="false" afterPath="$PROJECT_DIR$/database/files.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/database/models.go" beforeDir="false" afterPath="$PROJECT_DIR$/database/models.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/database/postgresql.go" beforeDir="false" afterPath="$PROJECT_DIR$/database/postgresql.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/files/filemanager.go" beforeDir="false" afterPath="$PROJECT_DIR$/files/filemanager.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/files/minio.go" beforeDir="false" afterPath="$PROJECT_DIR$/files/minio.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/files/staticanalysis.go" beforeDir="false" afterPath="$PROJECT_DIR$/files/staticanalysis.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/go.mod" beforeDir="false" afterPath="$PROJECT_DIR$/go.mod" afterDir="false" />
<change beforePath="$PROJECT_DIR$/go.sum" beforeDir="false" afterPath="$PROJECT_DIR$/go.sum" afterDir="false" />
<change beforePath="$PROJECT_DIR$/main.go" beforeDir="false" afterPath="$PROJECT_DIR$/main.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pgsetup.sql" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/podthing/downloadContainer.go" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/podthing/podmanager.go" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/sqliteschema.sql" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/templates/filelist.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/templates/filelist.gohtml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/templates/footer.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/templates/footer.gohtml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/web/browser.go" beforeDir="false" afterPath="$PROJECT_DIR$/web/browser/browser.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/web/filelist.go" beforeDir="false" afterPath="$PROJECT_DIR$/web/filelist.go" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -39,48 +77,48 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;DefaultGoTemplateProperty&quot;: &quot;Go File&quot;,
&quot;DefaultHtmlFileTemplate&quot;: &quot;HTML File&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.formatter.settings.were.checked&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.migrated.go.modules.settings&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.modules.automatic.dependencies.download&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.modules.go.list.on.any.changes.was.set&quot;: &quot;true&quot;,
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;go.import.settings.migrated&quot;: &quot;true&quot;,
&quot;go.sdk.automatically.set&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;/root&quot;,
&quot;list.type.of.created.stylesheet&quot;: &quot;CSS&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;ruby.rails.projectView.checked&quot;: &quot;true&quot;,
&quot;run.code.analysis.last.selected.profile&quot;: &quot;pProject Default&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;database.query.execution&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"DefaultGoTemplateProperty": "Go File",
"DefaultHtmlFileTemplate": "HTML File",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.go.formatter.settings.were.checked": "true",
"RunOnceActivity.go.migrated.go.modules.settings": "true",
"RunOnceActivity.go.modules.automatic.dependencies.download": "true",
"RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
"WebServerToolWindowFactoryState": "false",
"git-widget-placeholder": "main",
"go.import.settings.migrated": "true",
"go.sdk.automatically.set": "true",
"last_opened_file_path": "/home/johannes/git/filegate/filegate/templates",
"list.type.of.created.stylesheet": "CSS",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"ruby.rails.projectView.checked": "true",
"run.code.analysis.last.selected.profile": "pProject Default",
"settings.editor.selected.configurable": "preferences.lookFeel",
"vue.rearranger.settings.migration": "true"
},
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;sqlite&quot;
"keyToStringList": {
"DatabaseDriversLRU": [
"sqlite"
],
&quot;RunConfigurationTargetLRU&quot;: [
&quot;28e7aa7f-2fe3-45ee-997f-c35496a4a3eb/database/\&quot;filegate\&quot;&quot;
"RunConfigurationTargetLRU": [
"28e7aa7f-2fe3-45ee-997f-c35496a4a3eb/database/\"filegate\""
],
&quot;com.intellij.ide.scratch.LRUPopupBuilder$1/&quot;: [
&quot;SQLite&quot;
"com.intellij.ide.scratch.LRUPopupBuilder$1/": [
"SQLite"
],
&quot;com.intellij.ide.scratch.LRUPopupBuilder$1/SQL Dialect&quot;: [
&quot;SQLite&quot;
"com.intellij.ide.scratch.LRUPopupBuilder$1/SQL Dialect": [
"SQLite"
]
}
}</component>
}]]></component>
<component name="RdControllerToolWindowsLayoutState" isNewUi="true">
<layout>
<window_info id="Space Code Reviews" />
@ -118,10 +156,11 @@
<recent name="$PROJECT_DIR$/files" />
</key>
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/templates" />
<recent name="$PROJECT_DIR$" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/web/browser" />
<recent name="$PROJECT_DIR$/" />
<recent name="$PROJECT_DIR$/src/" />
<recent name="$PROJECT_DIR$/database" />
@ -204,11 +243,11 @@
</configuration>
<recent_temporary>
<list>
<item itemvalue="Database Script.pgsetup.sql" />
<item itemvalue="Go Build.go run ." />
<item itemvalue="Docker.devel-compose.yml: Compose Deployment" />
<item itemvalue="Docker.Dockerfile" />
<item itemvalue="Docker.Dockerfile builder" />
<item itemvalue="Go Build.go run ." />
<item itemvalue="Database Script.pgsetup.sql" />
</list>
</recent_temporary>
</component>
@ -246,12 +285,26 @@
<workItem from="1692525884012" duration="3960000" />
<workItem from="1692549204776" duration="6122000" />
<workItem from="1694330563801" duration="7776000" />
<workItem from="1695542369457" duration="4674000" />
<workItem from="1695990751089" duration="514000" />
<workItem from="1695991319785" duration="59000" />
<workItem from="1695991389809" duration="448000" />
<workItem from="1695991846569" duration="902000" />
<workItem from="1696234645954" duration="4224000" />
<workItem from="1696248724514" duration="4736000" />
<workItem from="1696311727002" duration="6423000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="UnknownFeatures">
<option featureType="dependencySupport" implementationName="executable:docker" />
<option featureType="dependencySupport" implementationName="executable:podman" />
<option featureType="dependencySupport" implementationName="executable:kubectl" />
<option featureType="dependencySupport" implementationName="executable:terraform" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>

View File

@ -3,7 +3,7 @@ LABEL authors="Johannes Bülow <johannes.buelow@jmbit.de>"
# Get all the stuff for CGO and Podman bindings
WORKDIR /usr/local/src
COPY ./ ./
RUN go get . && go mod download && go build -o filegate
RUN go get . && go mod download && go build -v -x -race -o filegate
FROM alpine
LABEL authors="Johannes Bülow <johannes.buelow@jmbit.de>"

View File

@ -1,22 +1,27 @@
# Installation
## Prerequisites
OS: Linux, preferrably Debian
Database: PostgreSQL
Object Storage: Preferrably MinIO, but anny S3-Compatible Storage should do
Container Engine: Podman
## Installation
### 1. Install the OS
This depends a lot on the Distribution you are using, but should usually be documented there
### 2. Install required Packages
For Debian, this would be:
```shell
export DEBIAN_FRONTEND="noninteractive"
apt-get update
apt-get upgrade -y
apt-get install podman file -y
apt-get install podman file postgres-y
# Please check the official install instructions for your up to date minio Version
wget https://dl.min.io/server/minio/release/linux-amd64/archive/minio_20230816201730.0.0_amd64.deb -O minio.deb
dpkg -i minio.deb
@ -24,10 +29,19 @@ curl https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
chmod +x /usr/local/bin/mc
```
Enable the podman socket for your user:
```shell
systemctl --user enable --now podman.socket
```
### 3. Configure Minio
More advanced Information can be found in the [Minio Documentation](https://min.io/docs/minio/linux/operations/installation.html)
More advanced Information can be found in
the [Minio Documentation](https://min.io/docs/minio/linux/operations/installation.html)
Example `/etc/default/minio`:
```shell
# MINIO_ROOT_USER and MINIO_ROOT_PASSWORD sets the root account for the MinIO server.
# This user has unrestricted permissions to perform S3 and administrative API operations on any resource in the deployment.
@ -46,24 +60,34 @@ MINIO_VOLUMES="/var/lib/minio"
# Uncomment the following line and replace the value with the correct hostname for the local machine and port for the MinIO server (9000 by default).
#MINIO_SERVER_URL="http://minio.example.net:9000"
MINIO_SERVER_URL="http://localhost:9000"
MINIO_CONSOLE_PORT=9001
```
#### 3.1. Create User
#### 3.2. Create Bucket
### 4. Configure PostgreSQL
Example script to set up PostgreSQL for Filegate:
*Slightly Jank (could have better permission management)*
```sql
CREATE DATABASE filegate;
CREATE user fgowner WITH PASSWORD 'supersecretpassword';
CREATE user fgwriter WITH PASSWORD 'alsoverysecretpassword';
CREATE USER fgreader WITH PASSWORD 'anothersecretpassword';
GRANT ALL PRIVILEGES ON DATABASE filegate TO fgowner WITH GRANT OPTION ;
GRANT ALL PRIVILEGES ON SCHEMA public TO fgowner WITH GRANT OPTION;
CREATE
DATABASE filegate;
CREATE
user filegate WITH PASSWORD 'supersecretpassword';
-- CREATE user fgwriter WITH PASSWORD 'alsoverysecretpassword';
--CREATE USER fgreader WITH PASSWORD 'anothersecretpassword';
GRANT ALL PRIVILEGES ON DATABASE
filegate TO filegate WITH GRANT OPTION;
GRANT ALL PRIVILEGES ON SCHEMA
public TO filegate WITH GRANT OPTION;
```
If you have changed the usernames for this database, you will also need to change them in `pgsetup.sql`
### 5. Install filegate itself
```shell
make
sudo make install
```

19
Makefile Normal file
View File

@ -0,0 +1,19 @@
build: dependencies
GOOS=linux go build -v -x -race -o filegate .
dependencies:
go get .
go mod download
run: dependencies
go run .
install: build
mkdir -P /etc/filegate
mkdir -P /var/local/filegate/
cp filegate /usr/local/bin/filegate
cp filegate.service /etc/systemd/system/filegate.service
useradd -d /var/local/filegate -m -U filegate
chownr -R filegate:filegate /var/local/filegate
systemctl daemon-reload
systemctl enable --now filegate
clean:
rm filegate

View File

@ -8,7 +8,11 @@ manual analysis
# Icon
Based on https://pictogrammers.com/library/mdi/icon/gate/ and https://pictogrammers.com/library/mdi/icon/floppy/
# AccessLevel
0 = User // can add files, view status of files created by them, edit comments, download cleared files
3 = Helpdesk // can see all files, full analysis of all files, can manually trigger steps
5 = Analyst // can do everything with files, but not change the software settings
10 = Admin // Can do literally anything
## Useful Links for Development
https://chenyitian.gitbooks.io/gin-tutorials/content/gin/8.html // Go Templates

View File

@ -1,20 +1,20 @@
serverhostname: filegate.dev.jmbit.de
serverport: 80
serverhostname: 0.0.0.0
serverport: 8080
loglevel: info
scratchdisk: "/var/tmp/filegate"
trustedproxies:
- 127.0.0.1
db:
hostname: filegate.dev.jmbit.de
port: 5432
owner: fgowner
ownerpassword: 0b26aca2-b10f-496e-9bd8-8bbfb143ad8b
writer: fgwriter
writerpassword: d5af6fde-7156-4ffa-ab0e-951df657990d
reader: fgreader
readerpassword: a788a837-f984-4b20-97db-b6dfd6b97a56
hostname: localhost
port: 5432
name: filegate
user: filegate
password: 80f9859c-d2fe-4864-a962-a58adbdcb5f3
minio:
accesskeyid: lZvkgrfXNbEMye6BSf6s
accesskeysecret: U109MtkE1jcc6qm3SIGk3IEZsq1cl8vTxqIRr3ZH
hostname: filegate.dev.jmbit.de
port: 9000
bucket: filegate
usessl: false
location: filegate-local
accesskeyid: lZvkgrfXNbEMye6BSf6s
accesskeysecret: U109MtkE1jcc6qm3SIGk3IEZsq1cl8vTxqIRr3ZH
hostname: 127.0.0.1
port: 9000
bucket: filegate
usessl: false
location: filegate-local

View File

@ -8,26 +8,36 @@ import (
var Cfg Config
func ParseConfig() {
cfg := getDefaults()
file, err := os.Open("config.yaml")
if os.Getenv("GIN_MODE") == "release" {
file, err = os.Open("/etc/filegate/config.yaml")
func init() {
ParseConfig()
if os.Getenv("GIN_MODE") != "release" {
log.Print(Cfg)
}
if err != nil && err.Error() == "open config.yaml: no such file or directory" {
createDefaultConfig()
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
}
}
}(file)
func ParseConfig() {
log.Println("Reading Config")
cfg := getDefaults()
configFile, err := os.ReadFile(findConfigFile())
if err != nil {
log.Println(err)
}
err = yaml.Unmarshal(configFile, &cfg)
if err != nil {
log.Println(err)
}
err = os.Mkdir(cfg.ScratchDisk, 600) // Directory to temporarily store files on host system
if err != nil {
log.Println(err)
}
Cfg = cfg
}
func createDefaultConfig() {
log.Println("Creating default config file")
file, err := os.Create("config.yaml")
if err != nil {
log.Fatalf("Could not create default config file: %v", err)
@ -44,3 +54,24 @@ func createDefaultConfig() {
log.Fatalf("failed to encode default Config YAML: %v", err)
}
}
// Checks first for a config file in the current directory, then in /etc/filegate/
func findConfigFile() string {
log.Println("Finding Config file")
if file, err := os.Stat("config.yaml"); err == nil {
log.Println(file)
return "config.yaml"
} else if file, err = os.Stat("config.yml"); err == nil {
log.Println(file)
return "config.yml"
} else if file, err = os.Stat("/etc/filegate/config.yaml"); err == nil {
log.Println(file)
return "/etc/filegate/config.yaml"
} else if file, err = os.Stat("/etc/filegate/config.yml"); err == nil {
log.Println(file)
return "/etc/filegate/config.yml"
} else {
createDefaultConfig()
return "config.yaml"
}
}

View File

@ -9,6 +9,9 @@ func getDefaults() Config {
ServerHostname: "localhost",
ServerPort: 8080,
LogLevel: "info",
ScratchDisk: "/var/tmp/filegate",
SessionSecret: uuid.NewString(),
TrustedProxies: []string{"127.0.0.1"},
DB: struct {
Hostname string
Port int

View File

@ -4,6 +4,9 @@ type Config struct {
ServerHostname string
ServerPort int
LogLevel string
ScratchDisk string
SessionSecret string
TrustedProxies []string
DB struct {
Hostname string
Port int

View File

@ -1,5 +1,11 @@
package database
import (
"github.com/gin-gonic/gin"
"log"
"strconv"
)
func CreateFile(name string, url string, comment string, blob string) (uint, error) {
file := File{
Name: name,
@ -31,6 +37,7 @@ func SetSimpleAttributes(id uint, mime string, size int64, sha256 string, sha1 s
file.Properties.Md5 = md5
file.Properties.FileCmd = fileCmd
file.Properties.Extension = extension
DB.Save(&file)
}
func CountFilesEntries() int64 {
@ -58,11 +65,31 @@ func GetFileBlob(id uint) string {
return file.Blob
}
func GetFileList(page int, count int) []File {
// TODO Implement paged queries to DB
_ = page
_ = count
func GetFileList(page int, count int, c *gin.Context) ([]File, error) {
query, err := searchQueryBuilder(c)
if err != nil {
return nil, err
}
var list []File
DB.Find(&list)
return list
DB.Limit(page * count).Where(query).Find(&list)
return list, nil
}
func searchQueryBuilder(c *gin.Context) (*File, error) {
// TODO Add parsing for all File properties
var query File
if c.Query("id") != "" {
id, err := strconv.ParseUint(c.Query("id"), 10, 0)
if err != nil {
log.Printf("Could not Convert ID from filter to int: %v", err)
return nil, err
}
query.ID = uint(id)
}
if c.Query("name") != "" {
query.Name = c.Query("name")
}
return &query, nil
}

View File

@ -59,6 +59,27 @@ type User struct {
Name string `gorm:"unique"`
DisplayName string
Email string
Oauth2Sub string
Role string
PassHash string
// Oauth2Sub string
Role UserRole
}
type UserRole struct {
gorm.Model
ID uint
Name string
AccessLevel int
}
type Pod struct {
gorm.Model
ID string
Name string
Type PodType
BoundPorts []int
}
type PodType struct {
ID uint
Name string
}

View File

@ -3,6 +3,7 @@ package database
import (
"fmt"
"git.jmbit.de/filegate/filegate/config"
gormsessions "github.com/gin-contrib/sessions/gorm"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"log"
@ -10,13 +11,15 @@ import (
var dbconf = config.Cfg.DB
var DB *gorm.DB
var SessionStore gormsessions.Store
func Connect() {
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable",
dbconf.Hostname, dbconf.User, dbconf.Password, dbconf.Name)
dbconf.Hostname, dbconf.User, dbconf.Password, dbconf.Name, dbconf.Port)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("Can't connect to DB: %v", err)
}
DB = db
SessionStore = gormsessions.NewStore(db, true, []byte(config.Cfg.SessionSecret))
}

57
database/user.go Normal file
View File

@ -0,0 +1,57 @@
package database
import "errors"
func CreateUser(name string, displayName string, email string, passHash string, role UserRole) error {
user := User{
Name: name,
DisplayName: displayName,
Email: email,
PassHash: passHash,
Role: role,
}
if err := DB.Create(&user).Error; err != nil {
return err
}
return nil
}
func DeleteUser(name string) error {
var user User
DB.First(&user, name)
if user.Name == "" {
return errors.New("user not found")
}
DB.Delete(&user)
return nil
}
func UpdatePassword(name string, passHash string) error {
var user User
DB.First(&user, name)
if user.Name == "" {
return errors.New("user not found")
}
user.PassHash = passHash
return nil
}
func UpdateEmail(name string, email string) error {
var user User
DB.First(&user, name)
if user.Name == "" {
return errors.New("user not found")
}
user.Email = email
return nil
}
func GetUser(name string) (User, error) {
var user User
DB.First(&user, name)
if user.Name == "" {
return user, errors.New("user not found")
}
return user, nil
}

17
filegate.service Normal file
View File

@ -0,0 +1,17 @@
[Unit]
Description=Enterprise Download Management Portal
After=postgresql.service
[Service]
Type=simple
User=filegate
Restart=always
RestartSec=3
PrivateTmp=yes
ExecStart=/usr/local/bin/filegate
ProtectSystem=strict
InaccessiblePaths=/root /home
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target

View File

@ -2,29 +2,12 @@ package files
import (
"context"
"fmt"
"git.jmbit.de/filegate/filegate/database"
"github.com/gin-gonic/gin"
"github.com/minio/minio-go/v7"
"log"
"os"
)
func StorageDirectory() {
if _, err := os.Stat("./filesstore"); os.IsNotExist(err) {
err := os.Mkdir("./filestore", 770)
if err != nil {
log.Println(fmt.Sprintf("Could not create File directory: %s", err))
}
}
if os.Getenv("GIN_MODE") != "release" {
err := os.RemoveAll("./filestore/*")
if err != nil {
return
}
}
}
func UploadFile(id uint, blob string, c *gin.Context) error {
ctx := context.Background()
file, err := c.FormFile("file")
@ -42,3 +25,11 @@ func UploadFile(id uint, blob string, c *gin.Context) error {
go RunStaticAnalysis(id)
return nil
}
// GetFile returns a pointer to a File stored in Minio by passing it the UUID ("Blob"-ID) of the file
func GetFile(uuid string) (*minio.Object, error) {
ctx := context.Background()
object, err := MinioClient.GetObject(ctx, minioConfig.Bucket, uuid, minio.GetObjectOptions{})
return object, err
}

View File

@ -14,6 +14,7 @@ var MinioClient = minioConnect()
func minioConnect() *minio.Client {
endpoint := fmt.Sprintf("%s:%d", minioConfig.Hostname, minioConfig.Port)
log.Printf("Minio Endpoint: %s", endpoint)
accessKeyID := minioConfig.AccessKeyID
accessKeySecret := minioConfig.AccessKeySecret

View File

@ -5,6 +5,7 @@ import (
"crypto/sha1"
"crypto/sha256"
"fmt"
"git.jmbit.de/filegate/filegate/config"
"git.jmbit.de/filegate/filegate/database"
"io"
"log"
@ -70,9 +71,40 @@ func fileSize(path string) int64 {
return filesize
}
func RunStaticAnalysis(id uint) {
//TODO: Use container instead of running on local disk
filepath := fmt.Sprintf("./filestore/%s", id)
// RunStaticAnalysis populates the File Properties Table for a given file
func RunStaticAnalysis(id uint) {
file := database.GetFileByID(id)
filepath := fmt.Sprintf("%s/%d/%s", config.Cfg.ScratchDisk, file.ID, file.Properties.OriginalName)
err := os.Mkdir(fmt.Sprintf("%s/%d", config.Cfg.ScratchDisk, file.ID), 700)
if err != nil {
log.Printf("Error Creating analysis directory, %v", err)
return
}
fileObject, err := GetFile(file.Blob)
if err != nil {
log.Printf("Error getting file from S3, %v", err)
fileObject.Close()
return
}
localFile, err := os.Create(filepath)
if err != nil {
log.Printf("Error Creating analysis file, %v", err)
fileObject.Close()
localFile.Close()
return
}
if _, err = io.Copy(localFile, fileObject); err != nil {
log.Printf("Could not Copy S3 File to local disk: %v", err)
fileObject.Close()
localFile.Close()
return
}
fileObject.Close()
localFile.Close()
mimeType := mimeType(filepath)
fileCmd := fileCmd(filepath)
fileExtension := fileExtension(filepath)

8
go.mod
View File

@ -4,10 +4,11 @@ go 1.20
require (
github.com/containers/podman/v4 v4.6.0
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.3.0
github.com/mattn/go-sqlite3 v1.14.17
github.com/minio/minio-go/v7 v7.0.62
golang.org/x/crypto v0.12.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.5.2
gorm.io/gorm v1.25.4
@ -71,8 +72,11 @@ require (
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-containerregistry v0.15.2 // indirect
github.com/google/go-intervals v0.0.2 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/schema v1.2.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@ -134,12 +138,12 @@ require (
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
github.com/vbauerster/mpb/v8 v8.4.0 // indirect
github.com/wader/gormstore/v2 v2.0.0 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.mongodb.org/mongo-driver v1.11.3 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.12.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.14.0 // indirect

510
go.sum

File diff suppressed because it is too large Load Diff

23
main.go
View File

@ -1,3 +1,5 @@
//go:build linux
package main
import (
@ -5,13 +7,13 @@ import (
"fmt"
"git.jmbit.de/filegate/filegate/config"
"git.jmbit.de/filegate/filegate/database"
"git.jmbit.de/filegate/filegate/files"
"git.jmbit.de/filegate/filegate/pods"
"git.jmbit.de/filegate/filegate/web"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"html/template"
"log"
"net/http"
"runtime"
)
//go:embed assets/jquery.min.js assets/custom.css assets/bootstrap/css/*.min.css assets/bootstrap/js/bootstrap.min.js assets/favicon.ico assets/logo.svg
@ -21,29 +23,19 @@ var assetsFS embed.FS
var templatesFS embed.FS
func main() {
if runtime.GOOS == "windows" {
fmt.Println("This Software is not supported on Windows")
}
config.ParseConfig()
addr := fmt.Sprintf("%s:%d", config.Cfg.ServerHostname, config.Cfg.ServerPort)
database.Connect()
// podthing.TestPodman()
files.StorageDirectory()
pods.TestPodman()
router := gin.Default()
templates := template.Must(template.New("").ParseFS(templatesFS, "templates/*.gohtml"))
router.ForwardedByClientIP = true
err := router.SetTrustedProxies([]string{"127.0.0.1"})
router.Use(sessions.Sessions("mySession", database.SessionStore))
err := router.SetTrustedProxies(config.Cfg.TrustedProxies)
if err != nil {
log.Println(err)
}
router.SetHTMLTemplate(templates)
router.StaticFS("/static", http.FS(assetsFS))
router.GET("/", web.GetIndex)
router.GET("/file/:id", web.GetFileView)
router.GET("/file/", web.GetFileList)
router.POST("/file/new", web.PostSubmitFileUrl)
router.POST("/file/upload", web.PostUploadFile)
router.GET("/browser/:id/")
router.GET("favicon.ico", func(c *gin.Context) {
file, _ := assetsFS.ReadFile("assets/favicon.ico")
c.Data(
@ -52,6 +44,7 @@ func main() {
file,
)
})
router = web.SetRoutes(router)
err = router.Run(addr)
if err != nil {
log.Fatal(err)

View File

@ -1,62 +0,0 @@
DROP TABLE IF EXISTS status CASCADE ;
DROP TABLE IF EXISTS file CASCADE ;
DROP TABLE IF EXISTS file_properties CASCADE ;
DROP TABLE IF EXISTS dynamic_analysis CASCADE ;
DROP TABLE IF EXISTS file_attachment CASCADE ;
-- DROP all tables (Only for development purposes
CREATE TABLE IF NOT EXISTS status
(
id INTEGER PRIMARY KEY,
name VARCHAR(64),
comment VARCHAR(2048)
);
CREATE TABLE IF NOT EXISTS file_properties
(
id SERIAL PRIMARY KEY,
sha256 VARCHAR(64),
sha1 VARCHAR(40),
md5 VARCHAR(32),
extension VARCHAR(80),
file_cmd VARCHAR(512),
original_name VARCHAR(256),
url VARCHAR(2048),
size BIGINT
);
CREATE TABLE IF NOT EXISTS file
(
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(64),
mime VARCHAR(256),
comment VARCHAR(2048),
status INT,
created BIGINT,
properties INT,
FOREIGN KEY (status)
REFERENCES status (id),
FOREIGN KEY (properties)
REFERENCES file_properties (id)
);
CREATE TABLE IF NOT EXISTS file_attachment
(
id SERIAL PRIMARY KEY,
type VARCHAR(32),
comment VARCHAR(2048),
file_id VARCHAR(36),
FOREIGN KEY (file_id)
REFERENCES file (id)
);
GRANT INSERT, UPDATE, DELETE ON file, file_attachment, file_properties, status TO fgwriter;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO fgreader;
INSERT INTO status (id, name, comment)
VALUES (0, 'New', 'Freshly uploaded file');
INSERT INTO status (id, name, comment)
VALUES (1, 'Processing', 'File is now being analyzed');
INSERT INTO status (id, name, comment)
VALUES (2, 'Denied', 'File download was denied');
INSERT INTO status (id, name, comment)
VALUES (3, 'Granted', 'File download was granted');

47
pods/autoAnalysis.go Normal file
View File

@ -0,0 +1,47 @@
package pods
import (
"fmt"
"git.jmbit.de/filegate/filegate/database"
"git.jmbit.de/filegate/filegate/files"
"github.com/containers/podman/v4/pkg/bindings/containers"
"github.com/containers/podman/v4/pkg/bindings/images"
"github.com/containers/podman/v4/pkg/specgen"
"log"
)
type AnalysisContainer struct {
UUID string
containerFile string
fileID uint
}
// CreateAutoAnalysisContainer creates a Static analysis Container with the file
// returns the UUID of the container and the path of the file
func CreateAutoAnalysisContainer(fileID uint) AnalysisContainer {
image := "git.jmbit.de/filegate/utility-containers:staticanalysis"
conn := socketConnection()
_, err := images.Pull(conn, image, nil)
if err != nil {
log.Println(err)
}
s := specgen.NewSpecGenerator(image, false)
createResponse, err := containers.CreateWithSpec(conn, s, nil)
if err != nil {
log.Println(err)
}
if err := containers.Start(conn, createResponse.ID, nil); err != nil {
log.Println(err)
}
file := database.GetFileByID(fileID)
fileObject, err := files.GetFile(file.Blob)
_, err = containers.CopyFromArchive(conn, createResponse.ID, fmt.Sprintf("/mnt/%s", file.Properties.OriginalName), fileObject)
if err != nil {
return AnalysisContainer{}
}
return AnalysisContainer{
UUID: createResponse.ID,
containerFile: fmt.Sprintf("/mnt/%s", file.Properties.OriginalName),
fileID: file.ID,
}
}

View File

@ -1,4 +1,4 @@
package podthing
package pods
import (
"github.com/containers/podman/v4/pkg/bindings/containers"

24
pods/podmanager.go Normal file
View File

@ -0,0 +1,24 @@
package pods
import (
"context"
"fmt"
"github.com/containers/podman/v4/pkg/bindings"
"log"
"os"
)
var Socket = socketConnection()
func socketConnection() context.Context {
uri := fmt.Sprintf("unix:///run/user/%d/podman/podman.sock", os.Getuid())
conn, err := bindings.NewConnection(context.Background(), uri)
if err != nil {
log.Fatal(err)
}
return conn
}
func TestPodman() {
_ = socketConnection()
}

View File

@ -1,45 +0,0 @@
package podthing
import (
"context"
"fmt"
"github.com/containers/podman/v4/pkg/bindings"
"github.com/containers/podman/v4/pkg/bindings/containers"
"github.com/containers/podman/v4/pkg/bindings/images"
"github.com/containers/podman/v4/pkg/specgen"
"log"
"os"
)
func socketConnection() context.Context {
uri := fmt.Sprintf("unix:///run/user/%d/podman/podman.sock", os.Getuid())
conn, err := bindings.NewConnection(context.Background(), uri)
if err != nil {
log.Println(err)
}
return conn
}
func TestPodman() {
conn := socketConnection()
_, err := images.Pull(conn, "quay.io/libpod/alpine_nginx", nil)
if err != nil {
log.Fatal(err)
}
s := specgen.NewSpecGenerator("quay.io/libpod/alpine_nginx", false)
s.Name = "fg-test"
createResponse, err := containers.CreateWithSpec(conn, s, nil)
if err != nil {
log.Fatal(err)
}
if err := containers.Start(conn, createResponse.ID, nil); err != nil {
log.Fatal(err)
}
if err := containers.Kill(conn, createResponse.ID, nil); err != nil {
log.Fatal(err)
}
if err, _ := containers.Remove(conn, createResponse.ID, nil); err != nil {
log.Fatal(err)
}
}

View File

@ -1,35 +0,0 @@
DROP TABLE IF EXISTS status;
DROP TABLE IF EXISTS files;
CREATE TABLE IF NOT EXISTS status (
id INTEGER PRIMARY KEY ,
name TEXT,
comment TEXT);
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
name TEXT,
url TEXT NOT NULL,
-- blob TEXT,
mime TEXT,
original_name TEXT,
comment TEXT,
size INTEGER,
sha256 TEXT,
sha1 TEXT,
md5 TEXT,
status_id INTEGER,
file_cmd TEXT,
created INTEGER,
extension TEXT,
FOREIGN KEY (status_id)
REFERENCES status (id)
ON UPDATE NO ACTION
ON DELETE NO ACTION
);
INSERT INTO status (id, name, comment)
VALUES(0, 'New', 'Freshly uploaded file');
INSERT INTO status (id, name, comment)
VALUES(1, 'Processing', '');
INSERT INTO status (id, name, comment)
VALUES(2, 'Denied', 'File download was denied');
INSERT INTO status (id, name, comment)
VALUES(3, 'Granted', 'File download was granted');

View File

@ -2,11 +2,19 @@
<div class="starter-template">
<div class="page-header">
<h1>Files</h1>
<p>List of all files and their Status</p>
<p>List of all files and their Status. Currently, can only search for ID and Name</p>
<p>Total Files: {{.totalEntries}}</p>
</div>
<div class="form-group">
<form action="/file/" method="GET">
<label for="name">Name</label>
<input type="text" name="name" id="name" class="form-control" required>
<label for="id">ID</label>
<input type="number" name="id" id="id" class="form-control" required>
<input type="submit" value="Search" class="btn btn-primary">
</form>
</div>
<div id="pagination"></div>
<table class="table table-hover">
<tbody>
<tr>
@ -35,43 +43,5 @@
</table>
</div>
<script>
// Generate pagination
function generatePagination(itemsPerPage, totalItems) {
// Calculate total pages
let totalPages = Math.ceil(totalItems / itemsPerPage);
let paginationHtml = '';
// Loop to generate page links
for (let i = 1; i <= totalPages; i++) {
paginationHtml += `
<li class="page-item">
<a class="page-link" href="#">${i}</a>
</li>
`;
}
// Output pagination
$('#pagination').html(paginationHtml);
}
// Usage
let itemsPerPage = {{.count}};
let totalItems = {{.totalEntries}};
generatePagination(itemsPerPage, totalItems);
$('#pagination').on('click', 'a', function(e){
// Get clicked page
let page = $(this).text();
// Load data for page
loadProducts(page);
e.preventDefault();
})
</script>
{{template "footer.gohtml"}}

View File

@ -1,6 +1,5 @@
<footer class="footer">
<p>Copyright &copy; Johannnes Bülow <a href="https://www.jmbit.de/">www.jmbit.de</a></p>
<p>Copyright &copy; Johannes Bülow <a href="https://www.jmbit.de/">www.jmbit.de</a></p>
</footer>
</div> <!-- End of main container -->

22
templates/login.gohtml Normal file
View File

@ -0,0 +1,22 @@
{{template "header.gohtml"}}
<div class="starter-template">
<div class="page-header">
<h1>{{ .title }}</h1>
</div>
<div class="row">
<div class="col-md-6">
<h2>Login</h2>
<div class="form-group">
<form action="/login" method="POST">
<label for="name">Enter your Username</label>
<input type="text" name="name" id="name" class="form-control" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" class="form-control" required>
<input type="submit" value="Login" class="btn btn-primary">
</form>
</div>
</div>
</div>
</div>
{{template "footer.gohtml"}}

57
web/auth/hashPassword.go Normal file
View File

@ -0,0 +1,57 @@
package auth
import (
"git.jmbit.de/filegate/filegate/database"
"github.com/gin-contrib/sessions"
"golang.org/x/crypto/bcrypt"
)
// adapted from https://www.gregorygaines.com/blog/how-to-properly-hash-and-salt-passwords-in-golang-bcrypt/
// hashPassword Hash password using bcrypt and
func hashPassword(password string) (string, error) {
// Convert password string to byte slice
var passwordBytes = []byte(password)
// Hash password with Bcrypt's default cost
hashedPasswordBytes, err := bcrypt.
GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
return string(hashedPasswordBytes), err
}
// doPasswordsMatch Check if two passwords match using Bcrypt's CompareHashAndPassword
// which return nil on success and an error on failure.
func doPasswordsMatch(hashedPassword, currPassword string) bool {
err := bcrypt.CompareHashAndPassword(
[]byte(hashedPassword), []byte(currPassword))
return err == nil
}
func storePasswordHash(username string, hash string) error {
err := database.UpdatePassword(username, hash)
if err != nil {
return err
}
return nil
}
// checkPassword checks if the password correctly correlates to the password hash stored in the Database
func checkPassword(username string, password string, session sessions.Session) error {
var user database.User
user, err := database.GetUser(username)
if err != nil {
return err
}
if doPasswordsMatch(user.PassHash, password) {
session.Set("username", username)
session.Set("accessLevel", user.Role.AccessLevel)
session.Set("isLoggedIn", true)
err := session.Save()
if err != nil {
return err
}
}
return nil
}

28
web/auth/login.go Normal file
View File

@ -0,0 +1,28 @@
package auth
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"log"
"net/http"
)
func GetLogin(c *gin.Context) {
content := gin.H{
"title": "Home",
}
c.HTML(http.StatusOK, "index.gohtml", content)
}
func PostLogin(c *gin.Context) {
session := sessions.Default(c)
name := c.PostForm("name")
password := c.PostForm("password")
err := checkPassword(name, password, session)
if err != nil {
c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{"title": "Error", "message": err})
log.Println(err)
return
}
}

31
web/auth/middleware.go Normal file
View File

@ -0,0 +1,31 @@
package auth
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"net/http"
)
// AuthMiddleware deals with checking authentication and authorization (Is the user logged in and permitted to see/do something)
func AuthMiddleware(requiredLevel int) gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
isLoggedIn := session.Get("isLoggedIn")
accessLevel := session.Get("accessLevel")
if isLoggedIn != true {
c.Redirect(http.StatusFound, "/login")
// Not logged in, abort
c.Abort()
return
}
if accessLevelValue, ok := accessLevel.(int); ok {
if accessLevelValue < requiredLevel {
c.HTML(http.StatusUnauthorized, "error.gohtml", gin.H{"title": "Not authorized", "message": "You are not authorized to do this action"})
c.Abort()
return
}
}
// Logged in and authorized, continue
c.Next()
}
}

View File

@ -1,13 +1,13 @@
package web
package browser
import (
"git.jmbit.de/filegate/filegate/podthing"
"git.jmbit.de/filegate/filegate/pods"
"github.com/google/uuid"
)
func NewBrowser() (string, string, string) {
passwd := uuid.NewString()
folderId := uuid.NewString()
browserPod := podthing.CreateDownloadContainer("user", passwd, folderId)
browserPod := pods.CreateDownloadContainer("user", passwd, folderId)
return browserPod, folderId, passwd
}

View File

@ -17,14 +17,17 @@ func GetFileList(c *gin.Context) {
if err != nil {
log.Println(err)
}
totalEntries := database.CountFilesEntries()
fileList := database.GetFileList(page, count)
fileList, err := database.GetFileList(page, count, c)
lowestEntry := (page - 1) * count
highestEntry := page * count
fileListPage := fileList[lowestEntry:highestEntry]
content := gin.H{
"fileList": fileList,
"error": "",
"fileList": fileListPage,
"error": err,
"page": page,
"count": count,
"totalEntries": totalEntries,
"totalEntries": len(fileList),
}
c.HTML(http.StatusOK, "filelist.gohtml", content)
}

17
web/routes.go Normal file
View File

@ -0,0 +1,17 @@
package web
import (
"git.jmbit.de/filegate/filegate/web/auth"
"github.com/gin-gonic/gin"
)
func SetRoutes(router *gin.Engine) *gin.Engine {
router.GET("/", GetIndex)
router.GET("/file/:id", GetFileView)
router.GET("/file/", GetFileList)
router.POST("/file/new", PostSubmitFileUrl)
router.POST("/file/upload", PostUploadFile)
router.GET("/browser/:id/")
router.GET("/login", auth.GetLogin)
return router
}

1
web/session.go Normal file
View File

@ -0,0 +1 @@
package web