diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..7b3c49a5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+resources/config.php
+resources/libraries/composer/
+resources/custom_user_mappings/*.csv
+bin/*.service
+
+.vscode/
\ No newline at end of file
diff --git a/README.md b/README.md
index 561731e5..8f45352a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,30 @@
-# unity-web-portal
-Web portal for the Unity Cluster
+# Unity Cluster Website #
+
+### Installation ###
+1. Prerequisites
+ 1. OpenLDAP Server with Admin Credentials, correct schema
+ 1. SQL Server, correct schema
+ 1. Slurm commands accessible on this host, `www-data` (or whatever the web server user is) should be an operator in `sacctmgr`
+ 1. SMTP Server
+ 1. Some HTTP Authentication mechanism (such as Shibboleth SP)
+1. Install required PHP Libraries:
+ 1. Install composer `apt install composer`
+ 1. Create directory and navigate to `resources/libraries/composer`
+ 1. Install phpmailer: `composer require phpmailer/phpmailer`
+ 1. Install phpseclib: `composer require phpseclib/phpseclib`
+ 1. Install php-ldap `apt install php-ldap`
+1. Setup config File `resources/config.php`, use the `resoures/config.php.example` as a reference
+1. Apache Configs
+
+
+
+#### Directory Structure ####
+* `/webroot` - Public root of the website (http document root)
+* `/resources` - Private directory containing php files not necessary to be public.
+
+The unity/webroot directory should be the **only** publicly accessible location (DocumentRoot in htdocs). The resources directory contains many php scripts that are referenced absolutely in the config.
+
+#### Server Setup ####
+This website has a public and private interface. The private interface is authenticated using a shibboleth SP. The following files/directories must be behind a shibboleth SP (configured through apache).
+* `/panel`
+* `/admin` for admins only
\ No newline at end of file
diff --git a/bin/skel/home/.bash_logout b/bin/skel/home/.bash_logout
new file mode 100644
index 00000000..a465a9d0
--- /dev/null
+++ b/bin/skel/home/.bash_logout
@@ -0,0 +1,7 @@
+# ~/.bash_logout: executed by bash(1) when login shell exits.
+
+# when leaving the console clear the screen to increase privacy
+
+if [ "$SHLVL" = 1 ]; then
+ [ -x /usr/bin/clear_console ] && /usr/bin/clear_console -q
+fi
\ No newline at end of file
diff --git a/bin/skel/home/.bashrc b/bin/skel/home/.bashrc
new file mode 100644
index 00000000..27a2f56e
--- /dev/null
+++ b/bin/skel/home/.bashrc
@@ -0,0 +1,117 @@
+# ~/.bashrc: executed by bash(1) for non-login shells.
+# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
+# for examples
+
+# If not running interactively, don't do anything
+case $- in
+ *i*) ;;
+ *) return;;
+esac
+
+# don't put duplicate lines or lines starting with space in the history.
+# See bash(1) for more options
+HISTCONTROL=ignoreboth
+
+# append to the history file, don't overwrite it
+shopt -s histappend
+
+# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
+HISTSIZE=1000
+HISTFILESIZE=2000
+
+# check the window size after each command and, if necessary,
+# update the values of LINES and COLUMNS.
+shopt -s checkwinsize
+
+# If set, the pattern "**" used in a pathname expansion context will
+# match all files and zero or more directories and subdirectories.
+#shopt -s globstar
+
+# make less more friendly for non-text input files, see lesspipe(1)
+[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"
+
+# set variable identifying the chroot you work in (used in the prompt below)
+if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
+ debian_chroot=$(cat /etc/debian_chroot)
+fi
+
+# set a fancy prompt (non-color, unless we know we "want" color)
+case "$TERM" in
+ xterm-color|*-256color) color_prompt=yes;;
+esac
+
+# uncomment for a colored prompt, if the terminal has the capability; turned
+# off by default to not distract the user: the focus in a terminal window
+# should be on the output of commands, not on the prompt
+#force_color_prompt=yes
+
+if [ -n "$force_color_prompt" ]; then
+ if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
+ # We have color support; assume it's compliant with Ecma-48
+ # (ISO/IEC-6429). (Lack of such support is extremely rare, and such
+ # a case would tend to support setf rather than setaf.)
+ color_prompt=yes
+ else
+ color_prompt=
+ fi
+fi
+
+if [ "$color_prompt" = yes ]; then
+ PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
+else
+ PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
+fi
+unset color_prompt force_color_prompt
+
+# If this is an xterm set the title to user@host:dir
+case "$TERM" in
+xterm*|rxvt*)
+ PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
+ ;;
+*)
+ ;;
+esac
+
+# enable color support of ls and also add handy aliases
+if [ -x /usr/bin/dircolors ]; then
+ test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
+ alias ls='ls --color=auto'
+ #alias dir='dir --color=auto'
+ #alias vdir='vdir --color=auto'
+
+ alias grep='grep --color=auto'
+ alias fgrep='fgrep --color=auto'
+ alias egrep='egrep --color=auto'
+fi
+
+# colored GCC warnings and errors
+#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
+
+# some more ls aliases
+alias ll='ls -alF'
+alias la='ls -A'
+alias l='ls -CF'
+
+# Add an "alert" alias for long running commands. Use like so:
+# sleep 10; alert
+alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"'
+
+# Alias definitions.
+# You may want to put all your additions into a separate file like
+# ~/.bash_aliases, instead of adding them here directly.
+# See /usr/share/doc/bash-doc/examples in the bash-doc package.
+
+if [ -f ~/.bash_aliases ]; then
+ . ~/.bash_aliases
+fi
+
+# enable programmable completion features (you don't need to enable
+# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
+# sources /etc/bash.bashrc).
+if ! shopt -oq posix; then
+ if [ -f /usr/share/bash-completion/bash_completion ]; then
+ . /usr/share/bash-completion/bash_completion
+ elif [ -f /etc/bash_completion ]; then
+ . /etc/bash_completion
+ fi
+fi
\ No newline at end of file
diff --git a/bin/skel/home/.profile b/bin/skel/home/.profile
new file mode 100644
index 00000000..456110de
--- /dev/null
+++ b/bin/skel/home/.profile
@@ -0,0 +1,27 @@
+# ~/.profile: executed by the command interpreter for login shells.
+# This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login
+# exists.
+# see /usr/share/doc/bash/examples/startup-files for examples.
+# the files are located in the bash-doc package.
+
+# the default umask is set in /etc/profile; for setting the umask
+# for ssh logins, install and configure the libpam-umask package.
+#umask 022
+
+# if running bash
+if [ -n "$BASH_VERSION" ]; then
+ # include .bashrc if it exists
+ if [ -f "$HOME/.bashrc" ]; then
+ . "$HOME/.bashrc"
+ fi
+fi
+
+# set PATH so it includes user's private bin if it exists
+if [ -d "$HOME/bin" ] ; then
+ PATH="$HOME/bin:$PATH"
+fi
+
+# set PATH so it includes user's private bin if it exists
+if [ -d "$HOME/.local/bin" ] ; then
+ PATH="$HOME/.local/bin:$PATH"
+fi
\ No newline at end of file
diff --git a/bin/skel/home/README b/bin/skel/home/README
new file mode 100644
index 00000000..1e4380ab
--- /dev/null
+++ b/bin/skel/home/README
@@ -0,0 +1,5 @@
+Hello Unity User!
+
+Welcome to the Unity cluster. If you're looking for documentation you can find that at https://unity.rc.umass.edu/docs. If you are looking for support please email hpc@umass.edu to reach our ticketing system, or fill out a form online at https://unity.rc.umass.edu/panel/contact.php.
+
+Enjoy!
\ No newline at end of file
diff --git a/bin/skel/scratch/README b/bin/skel/scratch/README
new file mode 100644
index 00000000..565addf4
--- /dev/null
+++ b/bin/skel/scratch/README
@@ -0,0 +1 @@
+This is your scratch home folder. FOLDERS AND FILES IN THIS DIRECTORY WILL AUTO DELETE AFTER 90 DAYS OF INACTIVITY. This folder is a much higher performance location. Cluster jobs should be run off of this location.
\ No newline at end of file
diff --git a/bin/unity_fs.py b/bin/unity_fs.py
new file mode 100644
index 00000000..a2421105
--- /dev/null
+++ b/bin/unity_fs.py
@@ -0,0 +1,315 @@
+#!/usr/bin/env python3
+
+import socket
+import xml.etree.ElementTree as ET
+import shutil
+import os
+import pwd
+import grp
+from _thread import *
+import requests
+from requests.auth import HTTPBasicAuth
+import urllib3
+import time
+import select
+
+
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+dirname = os.path.dirname(__file__)
+
+HOST = '127.0.0.1' # Standard loopback interface address (localhost)
+PORT = 2010 # Port to listen on (non-privileged ports are > 1023)
+
+NAS1_URL = "https://nas1.maas/api/v2.0"
+NAS1_API_KEY = "1-IYQsR0H2riooJKKy8c1kvjhUlBzkejeqoRUwP4PJiKwvtrDEmTZMwmQka9DEOZaC"
+NAS1_HOME_DATASET = "nas1-pool/home"
+NAS1_PROJECT_DATASET = "nas1-pool/project"
+NAS1_HEADERS = { 'Authorization': 'Bearer ' + NAS1_API_KEY }
+
+TIME_WAIT = 0.01
+TIMEOUT = 5 # timeout in seconds
+
+def threaded_client(conn):
+ NO_MESSAGE = 0
+
+ while True:
+ if (NO_MESSAGE * TIME_WAIT > TIMEOUT):
+ # timeout
+ break
+
+ try:
+ data = conn.recv(1024).decode('utf-8')
+
+ try:
+ tree = ET.ElementTree(ET.fromstring(data))
+ root = tree.getroot()
+ NO_MESSAGE = 0
+ except:
+ NO_MESSAGE += 1
+ time.sleep(TIME_WAIT)
+ continue
+
+ if root.tag != "request":
+ time.sleep(TIME_WAIT)
+ continue
+
+ req_type = root.attrib["type"]
+ if req_type == "populate_home":
+ dest = ""
+ user = ""
+ group = ""
+ scratch_loc = ""
+
+ for child in root:
+ if child.tag == "location":
+ dest = child.text
+ elif child.tag == "uid":
+ user = child.text
+ elif child.tag == "gid":
+ group = child.text
+ elif child.tag == "scratch":
+ scratch_loc = child.text
+
+ if not user.isdigit():
+ user = pwd.getpwnam(user).pw_uid
+ if not group.isdigit():
+ group = grp.getgrnam(group).gr_gid
+
+ source_home = os.path.join(dirname, 'skel/home')
+ for root, dirs, files in os.walk(source_home):
+ for file in files:
+ fullpath = os.path.join(root, file)
+ destpath = os.path.join(dest, file)
+ shutil.copy(fullpath, destpath)
+ os.chown(destpath, int(user), int(group))
+
+ # create symlink to scratch
+ if scratch_loc != "":
+ source_path = os.path.join(dest, "scratch")
+ os.symlink(scratch_loc, source_path)
+ os.chown(source_path, int(user), int(group), follow_symlinks = False)
+
+ conn.send("success".encode())
+
+ elif req_type == "populate_scratch":
+ dest = ""
+ user = ""
+ group = ""
+
+ for child in root:
+ if child.tag == "location":
+ dest = child.text
+ elif child.tag == "uid":
+ user = child.text
+ elif child.tag == "gid":
+ group = child.text
+
+ if not user.isdigit():
+ user = pwd.getpwnam(user).pw_uid
+ if not group.isdigit():
+ group = grp.getgrnam(group).gr_gid
+
+ source_scratch = os.path.join(dirname, 'skel/scratch')
+ for root, dirs, files in os.walk(source_scratch):
+ for file in files:
+ fullpath = os.path.join(root, file)
+ destpath = os.path.join(dest, file)
+ shutil.copy(fullpath, destpath)
+ os.chown(destpath, int(user), int(group))
+
+ conn.send("success".encode())
+
+ elif req_type == "create_home":
+ user = ""
+ group = ""
+ quota = ""
+
+ for child in root:
+ if child.tag == "uid":
+ user = child.text
+ elif child.tag == "gid":
+ group = child.text
+ elif child.tag == "quota":
+ quota = child.text
+
+ full_url = NAS1_URL + "/pool/dataset"
+ dataset_path = NAS1_HOME_DATASET + "/" + user
+ data = {
+ "name": dataset_path,
+ "quota": quota
+ }
+
+ # post request
+ result = requests.post(full_url, json = data, headers = NAS1_HEADERS, verify = False)
+ code = result.status_code
+ if code != 200 and code != 422:
+ error = "Error creating dataset"
+ conn.send(error.encode())
+ time.sleep(TIME_WAIT)
+ continue
+
+ # set perms
+ data = {
+ "user": user,
+ "group": group,
+ "acl": [
+ {
+ "tag": "owner@",
+ "id": None,
+ "type": "ALLOW",
+ "perms": {
+ "READ_DATA": True,
+ "WRITE_DATA": True,
+ "APPEND_DATA": True,
+ "READ_NAMED_ATTRS": True,
+ "WRITE_NAMED_ATTRS": True,
+ "DELETE_CHILD": True,
+ "READ_ATTRIBUTES": True,
+ "WRITE_ATTRIBUTES": True,
+ "DELETE": True,
+ "READ_ACL": True,
+ "WRITE_ACL": True,
+ "WRITE_OWNER": True,
+ "SYNCHRONIZE": True
+ },
+ "flags": {
+ "BASIC": "INHERIT"
+ }
+ },
+ {
+ "tag": "owner@",
+ "id": None,
+ "type": "ALLOW",
+ "perms": {
+ "EXECUTE": True
+ },
+ "flags": {"DIRECTORY_INHERIT": True}
+ },
+ {
+ "tag": "group@",
+ "id": None,
+ "type": "ALLOW",
+ "perms": {
+ "READ_DATA": True,
+ "READ_NAMED_ATTRS": True,
+ "READ_ATTRIBUTES": True,
+ "READ_ACL": True,
+ "SYNCHRONIZE": True
+ },
+ "flags": {"BASIC": "INHERIT"}
+ },
+ {
+ "tag": "group@",
+ "id": None,
+ "type": "ALLOW",
+ "perms": {
+ "EXECUTE": True
+ },
+ "flags": {"DIRECTORY_INHERIT": True}
+ },
+ {
+ "tag": "everyone@",
+ "id": None,
+ "type": "ALLOW",
+ "perms": {
+ "READ_NAMED_ATTRS": True,
+ "READ_ATTRIBUTES": True,
+ "READ_ACL": True
+ },
+ "flags": {"BASIC": "INHERIT"}
+ }
+ ],
+ "options": {
+ "stripacl": False,
+ "recursive": True,
+ "traverse": True
+ }
+ }
+
+ full_url = NAS1_URL + "/pool/dataset/id/" + dataset_path.replace('/', '%2F') + "/permission"
+ result = requests.post(full_url, json = data, headers = NAS1_HEADERS, verify = False)
+ code = result.status_code
+ if code != 200 and code != 422:
+ error = "Error setting dataset permissions"
+ conn.send(error.encode())
+ time.sleep(TIME_WAIT)
+ continue
+
+ full_url = NAS1_URL + "/sharing/nfs"
+ mnt_path = "/mnt/" + dataset_path
+ data = {
+ "paths": [mnt_path],
+ "comment": "",
+ "networks": [
+ "10.100.0.0/16",
+ "10.10.0.0/16"
+ ],
+ "hosts": [],
+ "alldirs": True,
+ "ro": False,
+ "quiet": False,
+ "maproot_user": "root",
+ "maproot_group": "wheel",
+ "mapall_user": "",
+ "mapall_group": "",
+ "security": [],
+ "enabled": True
+ }
+
+ result = requests.post(full_url, json = data, headers = NAS1_HEADERS, verify = False)
+ code = result.status_code
+ if code != 200 and code != 422:
+ error = "Error creating NFS export"
+ conn.send(error.encode())
+ time.sleep(TIME_WAIT)
+ continue
+
+ conn.send("Success".encode())
+
+ elif req_type == "create_scratch":
+ user = ""
+ group = ""
+
+ for child in root:
+ if child.tag == "uid":
+ user = child.text
+ elif child.tag == "gid":
+ group = child.text
+
+ scratch_path = "/scratch/" + user
+
+ if not user.isdigit():
+ user = pwd.getpwnam(user).pw_uid
+ if not group.isdigit():
+ group = grp.getgrnam(group).gr_gid
+
+ os.mkdir(scratch_path)
+
+ os.chown(scratch_path, int(user), int(group))
+
+ conn.send("Success".encode())
+
+ except Exception as e:
+ print(e)
+ xml_error = "" + str(e) + ""
+ conn.send(xml_error.encode())
+
+ time.sleep(TIME_WAIT)
+
+ conn.close()
+
+s = socket.socket()
+s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+try:
+ s.bind((HOST, PORT))
+except socket.error as e:
+ print(str(e))
+
+s.listen(5)
+
+while True:
+ (conn, addr) = s.accept()
+ start_new_thread(threaded_client, (conn, ))
+
+s.close()
\ No newline at end of file
diff --git a/bin/unityfs.service.example b/bin/unityfs.service.example
new file mode 100644
index 00000000..dfee09a5
--- /dev/null
+++ b/bin/unityfs.service.example
@@ -0,0 +1,11 @@
+[Unit]
+Description=Unity Filesystem Daemon
+After=network.target
+
+[Service]
+WorkingDirectory=
+ExecStart=python3 unity_fs.py
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
diff --git a/changelog b/changelog
new file mode 100644
index 00000000..41b7bb57
--- /dev/null
+++ b/changelog
@@ -0,0 +1,16 @@
+#### 0.5.0-BETA ####
+* [Feature] Added notices for inactive account
+* [Locale] New XML based locale for easier dynamic lang switching
+* [Feature] Added VAST storage and Truenas Core storage integration for automatic storage creation
+* [Feature] Added GitHub SSH key import
+* [Performance] Nested groups are loaded via AJAX instead of directly to avoid long loading times
+* [Feature] Added support for custom user id mappings
+* [Style] General style changes
+* [Feature] Added unityfs service, which is a socket service responsible for provisioning storage on Unity
+
+#### 0.4.0HF1-BETA ####
+* [Bug] Fixed issue where some users account wasn't activating
+
+#### 0.4.0-BETA ####
+* [Feature] Added Multiple PI Support
+* [Backend] Users and PI Groups are treated as different entities
\ No newline at end of file
diff --git a/etc/ldap/schema/unity_groups.schema b/etc/ldap/schema/unity_groups.schema
new file mode 100644
index 00000000..d8308757
--- /dev/null
+++ b/etc/ldap/schema/unity_groups.schema
@@ -0,0 +1,27 @@
+attributetype ( 1.3.6.1.4.1.8473.2.1.100000
+ NAME 'groupType'
+ DESC 'Group type'
+ EQUALITY caseExactMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32}
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.8473.2.1.100001
+ NAME 'groupOwner'
+ DESC 'Group owner UID'
+ EQUALITY caseExactMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32}
+ SINGLE-VALUE )
+
+attributetype ( 1.3.6.1.4.1.8473.2.1.100002
+ NAME 'groupRequests'
+ DESC 'UIDs that have requested to join this group'
+ EQUALITY caseExactMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32}
+ MULTI-VALUE )
+
+objectclass ( 1.3.6.1.4.1.8473.2.2.100000
+ NAME 'unityGroup'
+ DESC 'Unity Group Class'
+ SUP ( top $ posixGroup )
+ STRUCTURAL
+ MUST ( cn $ groupType $ groupOwner ) )
\ No newline at end of file
diff --git a/resources/autoload.php b/resources/autoload.php
new file mode 100644
index 00000000..3233921b
--- /dev/null
+++ b/resources/autoload.php
@@ -0,0 +1,55 @@
+here");
+}
+
+require_once config::PATHS["templates"] . "/globals.php";
+
+require_once config::PATHS["libraries"] . "/slurm.php";
+require_once config::PATHS["libraries"] . "/unityfs.php";
+require_once config::PATHS["libraries"] . "/unity-ldap.php";
+require_once config::PATHS["libraries"] . "/unity-user.php";
+require_once config::PATHS["libraries"] . "/unity-account.php";
+require_once config::PATHS["libraries"] . "/unity-sql.php";
+require_once config::PATHS["libraries"] . "/template_mailer.php";
+
+require_once config::PATHS["libraries"] . "/unity-service.php";
+
+$SERVICE = new serviceStack();
+$SERVICE->add_ldap(config::LDAP);
+$SERVICE->add_sql(config::SQL);
+$SERVICE->add_mail(config::MAIL);
+$SERVICE->add_sacctmgr(config::SLURM);
+$SERVICE->add_unityfs(config::UNITYFS);
+
+if (isset($_SERVER["REMOTE_USER"])) { // Check if SHIB is enabled on this page
+ // Set Shibboleth Session Vars - Vars stored in session to be accessible outside shib-controlled areas of the sites (ie contact page)
+ $SHIB = array(
+ "netid" => EPPN_to_uid($_SERVER["REMOTE_USER"]),
+ "firstname" => $_SERVER["givenName"],
+ "lastname" => $_SERVER["sn"],
+ "name" => $_SERVER["givenName"] . " " . $_SERVER["sn"],
+ "mail" => isset($_SERVER["mail"]) ? $_SERVER["mail"] : $_SERVER["eppn"] // Fallback to EPPN if mail is not set
+ );
+ $_SESSION["SHIB"] = $SHIB; // Set the session var for non-authenticated pages
+
+ $USER = new unityUser($SHIB["netid"], $SERVICE);
+ $_SESSION["user_exists"] = $USER->exists();
+ $_SESSION["is_pi"] = $USER->isPI();
+ $_SESSION["is_admin"] = $USER->isAdmin();
+}
+
+// Load Locale
+require_once config::PATHS["locale"] . "/" . config::LOCALE . ".php"; // Loads the locale class
\ No newline at end of file
diff --git a/resources/config.php.example b/resources/config.php.example
new file mode 100644
index 00000000..9b53db53
--- /dev/null
+++ b/resources/config.php.example
@@ -0,0 +1,85 @@
+ "https://github.com/UMass-RC/unity-web-portal",
+ "version" => "0.5.0-BETA"
+ );
+
+ public const PATHS = array(
+ "templates" => ROOT . "/resources/templates",
+ "libraries" => ROOT . "/resources/libraries",
+ "locale" => ROOT . "/resources/locale",
+ "skel" => ROOT . "/resources/etc/skel",
+ "custom_user" => ROOT . "/resources/custom_user_mappings"
+ );
+
+ public const LDAP = array(
+ "bind_dn" => "",
+ "bind_pass" => "",
+ "uri" => ""
+ );
+
+ public const SQL = array(
+ "host" => "",
+ "user" => "",
+ "pass" => "",
+ "db" => ""
+ );
+
+ public const SLURM = array(
+ "cluster" => ""
+ );
+
+ public const MAIL = array(
+ "host" => "",
+ "smtp_user" => "",
+ "smtp_pass" => "",
+ "smtp_secure" => "",
+ "port" => "",
+ "template_path" => config::PATHS["templates"] . "/mail",
+ "addresses" => array(
+ "contact" => array("", ""),
+ "sender" => array("", "")
+ )
+ );
+
+ public const UNITYFS = array(
+ "host" => "",
+ "port" => ""
+ );
+
+ public const SUPPORT = array(
+ "sendmail" => ""
+ );
+
+ public const LOCALE = "en";
+
+ public const CLUSTER = array(
+ "name" => "",
+ "org" => "",
+ "desc" => ""
+ );
+}
diff --git a/resources/custom_user_mappings/README b/resources/custom_user_mappings/README
new file mode 100644
index 00000000..7d717a4c
--- /dev/null
+++ b/resources/custom_user_mappings/README
@@ -0,0 +1,5 @@
+Put any csv in this folder in the format:
+
+,
+
+and the website will auto assign that id (assuming it is available) when that user applies for an account.
\ No newline at end of file
diff --git a/resources/libraries/ldap.php b/resources/libraries/ldap.php
new file mode 100644
index 00000000..3deacb87
--- /dev/null
+++ b/resources/libraries/ldap.php
@@ -0,0 +1,438 @@
+
+ * @version 1.0.0
+ * @since 7.2.0
+ */
+class ldapEntry {
+ private $conn; // LDAP connection link
+ private $dn; // Distinguished Name of the Entry
+
+ private $object; // Array containing the attributes of the entry as it looks on the server
+ private $mods; // Array containing modifications to $object array that have yet to be applied
+
+ /**
+ * Constructor that creates an ldapEntry object
+ *
+ * @param link_identifier $conn LDAP connection link from ldap_connect, ldap_bind must have already been used
+ * @param string $dn Distinguished Name of the requested entry
+ */
+ public function __construct($conn, $dn) {
+ $this->conn = $conn;
+ $this->dn = $dn;
+ $this->pullObject();
+ }
+
+ /**
+ * Pulls an entry from the ldap connection, and sets $object If entry does not exist, $object = null.
+ */
+ private function pullObject() {
+ $search = @ldap_get_entries($this->conn, ldap_read($this->conn, $this->dn, "(objectclass=*)"));
+ ldapConn::stripCount($search);
+
+ if (isset($search)) {
+ // Object Exists
+ if (count($search) > 1) { // 1 For LDAP count element, and 1 for actual object
+ // Duplicate Objects Found
+ die("FATAL: Call to ldapObject with non-unique DN.");
+ } else {
+ $this->object = $search[0];
+ }
+ }
+ }
+
+ /**
+ * Gets the Distinguished Name (DN) of the Entry
+ *
+ * @return string DN of the entry
+ */
+ public function getDN() {
+ return $this->dn;
+ }
+
+ /**
+ * Gets the Relative Distinguished Name (RDN) of the Entry
+ *
+ * @return string RDN of the entry
+ */
+ public function getRDN() {
+ return substr($this->dn, 0, strpos($this->dn, ','));
+ }
+
+ /**
+ * Checks whether entry exists on the LDAP server, modifications that haven't been applied don't count
+ *
+ * @return bool True if entry exists, False if it does not exist
+ */
+ public function exists() {
+ return !is_null($this->object);
+ }
+
+ /**
+ * Writes changes set in $mods array to the LDAP entry on the server.
+ *
+ * @return bool True on success, False on failure
+ */
+ public function write() {
+ if ($this->object == NULL) {
+ $success = ldap_add($this->conn, $this->dn, $this->mods); // Create a New Entry
+ } else {
+ if ($this->mods == NULL) {
+ throw new Exception("No modifications were made");
+ } else {
+ $success = ldap_mod_replace($this->conn, $this->dn, $this->mods); // Modify Existing Entry
+ }
+ }
+
+ if ($success) {
+ $this->pullObject(); // Refresh $object array
+ $this->mods = NULL; // Reset Modifications Array to Null
+ }
+ return $success;
+ }
+
+ /**
+ * Deletes the entry (no need to call write())
+ *
+ * @return bool True on success, False on failure
+ */
+ public function delete() {
+ if ($this->object == NULL) {
+ return true;
+ } else {
+ if(ldap_delete($this->conn, $this->dn)) {
+ $this->pullObject();
+ $this->mods = NULL;
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Moves the entry to a new location
+ *
+ * @param string $destination Destination CN to move this entry
+ * @return mixed ldapEntry of the new entry if successful, false on failure
+ */
+ public function move($destination) {
+ $newRDN = substr($destination, 0, strpos($destination, ','));
+ $newParent = substr($destination, strpos($destination, ',') + 1);
+ if (ldap_rename($this->conn, $this->dn, $newRDN, $newParent, true)) {
+ $this->pullObject(); // Refresh the existing entry
+ return new ldapEntry($this->conn, $destination);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Gets the immediate parent of the entry
+ *
+ * @return ldapEntry The parent of the current Entry
+ */
+ public function getParent() {
+ return new ldapEntry($this->conn, substr($this->dn, strpos($this->dn, ',') + 1)); //TODO edge case for parent being non-existent (part of base dn)
+ }
+
+ /**
+ * Gets an array of children of the entry
+ *
+ * @param boolean $recursive (optional) If true, recursive search. Default is false.
+ * @param string $filter (optional) Filter matching LDAP search filter syntax
+ * @return array Array of children entries
+ */
+ public function getChildrenArray($recursive = false, $filter = "(objectclass=*)") {
+ if ($recursive) {
+ $search = ldap_search($this->conn, $this->dn, $filter);
+ } else {
+ $search = ldap_list($this->conn, $this->dn, $filter);
+ }
+
+ $search_entries = @ldap_get_entries($this->conn, $search);
+ ldapConn::stripCount($search_entries);
+
+ if (count($search_entries) > 0 && $search_entries[0]["dn"] == $this->getDN()) {
+ array_shift($search_entries);
+ }
+
+ return $search_entries;
+ }
+
+ /**
+ * Gets an array of the children of the entry saved as ldapEntry class
+ *
+ * @param bool $recursive (optional) If true, recursive search. Default is false.
+ * @param string $filter (optional) Filter matching LDAP search filter syntax
+ * @return array Array of children ldapEntry objects
+ */
+ public function getChildren($recursive = false, $filter = "(objectclass=*)") {
+ $children_array = $this->getChildrenArray($recursive, $filter);
+
+ $output = array();
+ foreach ($children_array as $child) {
+ array_push($output, new ldapEntry($this->conn, $child["dn"]));
+ }
+
+ return $output;
+ }
+
+ /**
+ * Gets a single child using RDN
+ *
+ * @param string $rdn RDN of requested child
+ * @return ldapEntry object of the child
+ */
+ public function getChild($rdn) {
+ return new ldapEntry($this->conn, $rdn . "," . $this->dn);
+ }
+
+ /**
+ * Checks if entry has any children
+ *
+ * @return boolean True if yes, False if no
+ */
+ public function hasChildren() {
+ return count($this->getChildrenArray()) > 0;
+ }
+
+ /**
+ * Gets the number of children of the entry
+ *
+ * @param boolean $recursive (optional) If true, recursive search. Default is false.
+ * @return int Number of children of entry
+ */
+ public function numChildren($recursive = false) {
+ return count($this->getChildrenArray($recursive));
+ }
+
+ /**
+ * Sets the value of a single attribute in the LDAP object (This will overwrite any existing values in the attribute)
+ *
+ * @param string $attr Attribute Key Name to modify
+ * @param mixed $value array or string value to set the attribute value to
+ */
+ public function setAttribute($attr, $value) {
+ if (is_array($value)) {
+ $this->mods[$attr] = $value;
+ } else {
+ $this->mods[$attr] = array($value);
+ }
+ }
+
+ /**
+ * Appends values to a given attribute, preserving initial values in the attribute
+ *
+ * @param string $attr Attribute Key Name to modify
+ * @param mixed $value array or string value to append attribute
+ */
+ public function appendAttribute($attr, $value) {
+ $objArr = array();
+ if (isset($this->object[$attr])) {
+ $objArr = $this->object[$attr];
+ }
+
+ $modArr = array();
+ if (isset($this->mods[$attr])) {
+ $modArr = $this->mods[$attr];
+ }
+
+ if (is_array($value)) {
+ $this->mods[$attr] = array_merge($objArr, $modArr, $value);
+ } else {
+ $this->mods[$attr] = array_merge($objArr, $modArr, (array) $value);
+ }
+ }
+
+ /**
+ * Sets and overwrites attributes based on a single array.
+ *
+ * @param array $arr Array of keys and attributes. Key values must be attribute key
+ */
+ public function setAttributes($arr) {
+ $this->mods = $arr;
+ }
+
+ /**
+ * Appends attributes based on a single array
+ *
+ * @param array $arr Array of keys and attributes. Key values must be attribute key
+ */
+ public function appendAttributes($arr) {
+ foreach($arr as $attr) {
+ $this->appendAttribute(key($attr), $attr);
+ }
+ }
+
+ /**
+ * Removes a attribute
+ *
+ * @param string $attr Key of attribute to be removed
+ */
+ public function removeAttribute($attr, $item = NULL) {
+ $this->mods[$attr] = array();
+ }
+
+ /**
+ * Removes values of an attribute
+ *
+ * @param string $attr Attribute to modify
+ * @param string $value Value to erase from attribute
+ */
+ public function removeAttributeEntryByValue($attr, $value) {
+ $arr = $this->object[$attr];
+ for ($i = 0; $i < count($arr); $i++) {
+ if ($arr[$i] == $value) {
+ unset($arr[$i]);
+ }
+ }
+ $this->mods[$attr] = array_values($arr);
+ }
+
+ /**
+ * Returns a given attribute of the object
+ *
+ * @param string $attr Attribute key value to return
+ * @return array value of requested attribute. Note: lots of attributes are arrays by default, so you have to use index 0 of the return value to get a single value
+ */
+ public function getAttribute($attr) {
+ if (isset($this->object[$attr])) {
+ return $this->object[$attr];
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Returns the entire objects attributes
+ *
+ * @return array Array where keys are attributes
+ */
+ public function getAttributes() {
+ return $this->object;
+ }
+
+ /**
+ * Checks if entry has an attribute
+ *
+ * @param string $attr Attribute to check
+ * @return bool true if attribute exists in entry, false otherwise
+ */
+ public function hasAttribute($attr) {
+ if ($this->exists()) {
+ return array_key_exists($attr, $this->object);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if an attribute value exists within an attribute
+ *
+ * @param string $attr Attribute to check
+ * @param string $value Value to check
+ * @return bool true if value exists in attribute, false otherwise
+ */
+ public function attributeValueExists($attr, $value) {
+ return in_array($value, $this->getAttribute($attr));
+ }
+
+ /**
+ * Check if there are pending changes
+ *
+ * @return bool true is there are pending changes, false otherwise
+ */
+ public function pendingChanges() {
+ return !is_null($this->mods);
+ }
+}
+
+/**
+ * Class that represents a connection to an LDAP server
+ *
+ * Originally written for UMASS Amherst Research Computing
+ *
+ * @author Hakan Saplakoglu
+ * @version 1.0.0
+ * @since 7.2.0
+ */
+class ldapConn {
+ protected $conn; // LDAP link
+
+ /**
+ * Constructor, starts an ldap connection and binds to a DN
+ *
+ * @param string $host Host ldap address of server
+ * @param string $bind_dn Admin bind dn
+ * @param string $bind_pass Admin bind pass
+ */
+ public function __construct($host, $bind_dn, $bind_pass) {
+ $this->conn = ldap_connect($host);
+
+ ldap_set_option($this->conn, LDAP_OPT_PROTOCOL_VERSION, 3);
+ ldap_bind($this->conn, $bind_dn, $bind_pass);
+ }
+
+ /**
+ * Get the connection instance of the LDAP link
+ *
+ * @return link_identifier LDAP connection link
+ */
+ public function getConn() {
+ return $this->conn;
+ }
+
+ /**
+ * Runs a search on the LDAP server and returns entries
+ *
+ * @param string $filter LDAP_search filter
+ * @param string $base Search base
+ * @return array Array of ldapEntry objects
+ */
+ public function search($filter, $base, $recursive = true) {
+ if ($recursive) {
+ $search = ldap_search($this->conn, $base, $filter);
+ } else {
+ $search = ldap_list($this->conn, $base, $filter);
+ }
+
+ $search_entries = @ldap_get_entries($this->conn, $search);
+ self::stripCount($search_entries);
+
+ $output = array();
+ for($i = 0; isset($search_entries) && $i < count($search_entries); $i++) {
+ array_push($output, new ldapEntry($this->conn, $search_entries[$i]["dn"]));
+ }
+
+ return $output;
+ }
+
+ /**
+ * Gets a single entry from the LDAP server
+ *
+ * @param string $dn Distinguished name (DN) of requested entry
+ * @return ldapEntry requested entry object
+ */
+ public function getEntry($dn) {
+ return new ldapEntry($this->conn, $dn);
+ }
+
+ /**
+ * Removes the very annoying "count" attribute that comes out of all ldap search queries (why does that exist? Every language I know can figure out the count itself)
+ *
+ * @param array $arr Array passed by reference to modify
+ */
+ public static function stripCount(&$arr) {
+ if(is_array($arr)) {
+ unset($arr['count']);
+ array_walk($arr, "ldapConn::stripCount");
+ }
+ }
+}
diff --git a/resources/libraries/slurm.php b/resources/libraries/slurm.php
new file mode 100644
index 00000000..ed4ee9c6
--- /dev/null
+++ b/resources/libraries/slurm.php
@@ -0,0 +1,151 @@
+cluster = $clustername;
+ }
+
+ private static function cmd($cmd)
+ {
+ exec($cmd, $output, $return);
+
+ if ($return != 0) {
+ throw new Exception("$cmd returned error code $return with output " . var_dump($output));
+ }
+
+ return $output;
+ }
+
+ public function addAccount($name)
+ {
+ if (!$this->accountExists($name)) {
+ self::cmd(self::CMD_PREFIX . "add account $name Cluster=$this->cluster");
+ }
+ }
+
+ public function deleteAccount($name)
+ {
+ if ($this->accountExists($name)) {
+ self::cmd(self::CMD_PREFIX . "delete account $name");
+ }
+ }
+
+ public function addUser($name, $account)
+ {
+ if (!$this->userExists($name, $account)) {
+ self::cmd(self::CMD_PREFIX . "add user $name Account=$account");
+ }
+ }
+
+ public function deleteUser($name, $account)
+ {
+ if ($this->userExists($name, $account)) {
+ self::cmd(self::CMD_PREFIX . "delete user $name where Account=$account");
+ }
+ }
+
+ public function accountExists($name)
+ {
+ return count($this->readAssocDB(array("Account=$name"))) > 0;
+ }
+
+ public function userExists($name, $account = NULL)
+ {
+ if (is_null($account)) {
+ return count($this->readAssocDB(array("User=$name"))) > 0;
+ } else {
+ return count($this->readAssocDB(array("User=$name","Account=$account"))) > 0;
+ }
+ }
+
+ public function getAccountsFromUser($user) {
+ $accounts = $this->readAssocDB(array("User=$user"));
+
+ $out = array();
+ foreach($accounts as $account) {
+ if ($account[1] != "" && $account[1] != "root") {
+ array_push($out, $account[1]); // index 2 is UID
+ }
+ }
+
+ return $out;
+ }
+
+ public function getUsersFromAccount($account) {
+ $users = $this->readAssocDB(array("Account=$account"));
+
+ $out = array();
+ foreach($users as $user) {
+ if ($user[2] != "" && $user[2] != "root") {
+ array_push($out, $user[2]); // index 2 is UID
+ }
+ }
+
+ return $out;
+ }
+
+ public function getAccounts() {
+ $accounts_raw = $this->readAccDB();
+
+ $out = array();
+ foreach ($accounts_raw as $account_raw) {
+ $pi_netid = $account_raw[0];
+ if ($pi_netid != "root") { // disregard root account
+ array_push($out, $pi_netid);
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Updates the local var with cluster associations
+ */
+ private function readAssocDB($filters = array()) {
+ $query = self::CMD_PREFIX . "-P show associations"; // -P is parsable output
+
+ foreach ($filters as $filter) {
+ $query .= " where $filter";
+ }
+
+ $associations = self::cmd($query);
+ array_shift($associations); // Remove the key output
+
+ $out = array();
+ foreach($associations as $assoc) {
+ $exploded = explode("|", $assoc);
+ array_push($out, $exploded);
+ }
+
+ return $out;
+ }
+
+ private function readAccDB($filters = array()) {
+ $query = self::CMD_PREFIX . "-P show accounts"; // -P is parsable output
+
+ foreach ($filters as $filter) {
+ $query .= " where $filter";
+ }
+
+ $associations = self::cmd($query);
+ array_shift($associations); // Remove the key output
+
+ $out = array();
+ foreach($associations as $assoc) {
+ $exploded = explode("|", $assoc);
+ array_push($out, $exploded);
+ }
+
+ return $out;
+ }
+}
diff --git a/resources/libraries/template_mailer.php b/resources/libraries/template_mailer.php
new file mode 100644
index 00000000..366a82e4
--- /dev/null
+++ b/resources/libraries/template_mailer.php
@@ -0,0 +1,36 @@
+template_dir = $template_dir;
+ }
+
+ public function send($template = null, $data = null) {
+ if (isset($template)) {
+ ob_start();
+ require_once $this->template_dir . "/" . $template . ".php";
+ $mes_html = ob_get_clean();
+ $this->msgHTML($mes_html);
+ }
+
+ if (parent::send()) {
+ // Clear addresses
+ $this->clearAllRecipients();
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
+
+?>
diff --git a/resources/libraries/unity-account.php b/resources/libraries/unity-account.php
new file mode 100644
index 00000000..695672c2
--- /dev/null
+++ b/resources/libraries/unity-account.php
@@ -0,0 +1,265 @@
+
+ * @param unityLDAP $unityLDAP LDAP Connection
+ * @param unitySQL $unitySQL SQL Connection
+ * @param slurm $sacctmgr Slurm Connection
+ */
+ public function __construct($pi_uid, $service_stack)
+ {
+ $this->pi_uid = $pi_uid;
+
+ if (is_null($service_stack->ldap())) {
+ throw new Exception("LDAP is required for the unityUser class");
+ }
+
+ if (is_null($service_stack->sql())) {
+ throw new Exception("SQL is required for the unityUser class");
+ }
+
+ if (is_null($service_stack->sacctmgr())) {
+ throw new Exception("sacctmgr is required for the unityUser class");
+ }
+
+ if (is_null($service_stack->unityfs())) {
+ throw new Exception("unityfs is required for the unityUser class");
+ }
+
+ $this->service_stack = $service_stack;
+ }
+
+ /**
+ * Returns this group's PI UID
+ *
+ * @return string PI UID of the group
+ */
+ public function getPIUID() {
+ return $this->pi_uid;
+ }
+
+ /**
+ * Checks if the current PI is an approved and existent group
+ *
+ * @return bool true if yes, false if no
+ */
+ public function exists()
+ {
+ return $this->service_stack->sacctmgr()->accountExists($this->pi_uid);
+ }
+
+ public function createGroup()
+ {
+ // make this user a PI
+ $owner = $this->getOwner();
+
+ // (1) Create LDAP PI group
+ $ldapPiGroupEntry = $this->getLDAPPiGroup();
+
+ if (!$ldapPiGroupEntry->exists()) {
+ $nextGID = $this->service_stack->ldap()->getNextPiGID();
+
+ $ldapPiGroupEntry->setAttribute("objectclass", unityLDAP::POSIX_GROUP_CLASS);
+ $ldapPiGroupEntry->setAttribute("gidnumber", strval($nextGID));
+ $ldapPiGroupEntry->setAttribute("memberuid", array($owner->getUID())); // add current user as the first memberuid
+
+ if (!$ldapPiGroupEntry->write()) {
+ throw new Exception("Failed to create POSIX group for " . $owner->getUID());
+ }
+ }
+
+ // (2) Create slurm account
+ $this->createSlurmAccount();
+ $this->addAssociation(self::getUIDfromPIUID($this->pi_uid)); // add owner user
+ }
+
+ public function removeGroup() {
+ $this->service_stack->sql()->removeRequests($this->pi_uid); // remove any lasting requests
+
+ $users = $this->getGroupMembers();
+ foreach ($users as $user) {
+ $this->removeUserFromGroup($user);
+ }
+
+ // remove admin
+ $this->removeUserFromGroup($this->getOwner());
+
+ $ldapPiGroupEntry = $this->getLDAPPiGroup();
+ if ($ldapPiGroupEntry->exists()) {
+ if (!$ldapPiGroupEntry->delete()) {
+ throw new Exception("Unable to delete PI ldap group");
+ }
+ }
+
+ $this->removeSlurmAccount();
+ }
+
+ public function getOwner() {
+ return new unityUser(self::getUIDfromPIUID($this->pi_uid), $this->service_stack);
+ }
+
+ public function getLDAPPiGroup()
+ {
+ $group_entries = $this->service_stack->ldap()->pi_groupOU->getChildren(true, "(" . unityLDAP::RDN . "=" . $this->pi_uid . ")");
+
+ if (count($group_entries) > 0) {
+ return $group_entries[0];
+ } else {
+ return new ldapEntry($this->service_stack->ldap()->getConn(), unityLDAP::RDN . "=$this->pi_uid," . unityLDAP::PI_GROUPS);
+ }
+ }
+
+ public function addUserToGroup($new_user)
+ {
+ // Create Association
+ $this->addAssociation($new_user->getUID());
+
+ // Add to LDAP Group
+ $pi_group = $this->getLDAPPiGroup();
+ $pi_group->appendAttribute("memberuid", $new_user->getUID());
+ if (!$pi_group->write()) {
+ // failed to write
+ $this->removeAssociation($new_user->getUID());
+ }
+ }
+
+ public function removeUserFromGroup($old_user)
+ {
+ // Remove Association
+ $this->removeAssociation($old_user->getUID());
+
+ // Remove from LDAP Group
+ $pi_group = $this->getLDAPPiGroup();
+ $pi_group->removeAttributeEntryByValue("memberuid", $old_user->getUID());
+ if (!$pi_group->write()) {
+ // failed to write
+ $this->addAssociation($old_user->getUID());
+ }
+ }
+
+ //
+ // Slurm Related Functions
+ //
+
+ /**
+ * Creates a group based off this user (this user is a PI)
+ */
+ private function createSlurmAccount()
+ {
+ $this->service_stack->sacctmgr()->addAccount($this->pi_uid);
+ }
+
+ private function removeSlurmAccount()
+ {
+ $this->service_stack->sacctmgr()->deleteAccount($this->pi_uid);
+ }
+
+ private function addAssociation($uid)
+ {
+ if (!$this->service_stack->sacctmgr()->accountExists($this->pi_uid)) {
+ throw new Exception("Unable to create an association to a nonexist account $this->pi_uid");
+ }
+
+ // Add Slurm User
+ $this->service_stack->sacctmgr()->addUser($uid, $this->pi_uid);
+ }
+
+ /**
+ * Undocumented function
+ *
+ * @param [type] $netid
+ * @return void
+ */
+ private function removeAssociation($uid)
+ {
+ if (!$this->service_stack->sacctmgr()->userExists($uid, $this->pi_uid)) {
+ throw new Exception("Unable to remove association because an association doesn't exist");
+ }
+
+ $this->service_stack->sacctmgr()->deleteUser($uid, $this->pi_uid);
+ }
+
+
+ public function getAssociations()
+ {
+ return $this->service_stack->sacctmgr()->getUsersFromAccount($this->pi_uid);
+ }
+
+ public function getGroupMembers()
+ {
+ $members = $this->getAssociations();
+
+ $out = array();
+ $owner_uid = $this->getOwner()->getUID();
+ foreach ($members as $member) {
+ if ($member != $owner_uid) {
+ array_push($out, new unityUser($member, $this->service_stack));
+ }
+ }
+
+ return $out;
+ }
+
+ public static function getPIFromPIGroup($pi_netid)
+ {
+ if (substr($pi_netid, 0, strlen(self::PI_PREFIX)) == self::PI_PREFIX) {
+ return substr($pi_netid, strlen(self::PI_PREFIX));
+ } else {
+ throw new Exception("PI netid doesn't have the correct prefix.");
+ }
+ }
+
+ public function addRequest($uid)
+ {
+ $this->service_stack->sql()->addRequest($uid, $this->pi_uid);
+ }
+
+ public function removeRequest($uid)
+ {
+ $this->service_stack->sql()->removeRequest($uid, $this->pi_uid);
+ }
+
+ public function getRequests()
+ {
+ $requests = $this->service_stack->sql()->getRequests($this->pi_uid);
+
+ $out = array();
+ foreach ($requests as $request) {
+ array_push($out, new unityUser($request["uid"], $this->service_stack));
+ }
+
+ return $out;
+ }
+
+ public static function getPIUIDfromUID($uid)
+ {
+ return self::PI_PREFIX . $uid;
+ }
+
+ public static function getUIDfromPIUID($pi_uid)
+ {
+ if (substr($pi_uid, 0, strlen(self::PI_PREFIX)) == self::PI_PREFIX) {
+ return substr($pi_uid, strlen(self::PI_PREFIX));
+ } else {
+ throw new Exception("PI netid doesn't have the correct prefix.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/libraries/unity-ldap.php b/resources/libraries/unity-ldap.php
new file mode 100644
index 00000000..0916a107
--- /dev/null
+++ b/resources/libraries/unity-ldap.php
@@ -0,0 +1,205 @@
+userOU = $this->getEntry(self::USERS);
+ $this->groupOU = $this->getEntry(self::GROUPS);
+ $this->pi_groupOU = $this->getEntry(self::PI_GROUPS);
+ $this->adminGroup = $this->getEntry(self::ADMIN_GROUP);
+ }
+
+ public function getNextUID()
+ {
+ $users = $this->userOU->getChildrenArray(true); // ! Restore this instead of temporary
+
+ usort($users, function ($a, $b) {
+ return $a["uidnumber"] <=> $b["uidnumber"];
+ });
+
+ $id = self::ID_MAP[0];
+ foreach ($users as $acc) {
+ if ($id == $acc["uidnumber"][0]) {
+ $id++;
+ if ($id > self::ID_MAP[1]) {
+ throw new Exception("UID Limits reached"); // all hell has broken if this executes
+ }
+ } else {
+ break;
+ }
+ }
+
+ return $id;
+ }
+
+ public function getNextGID()
+ {
+ $groups = $this->groupOU->getChildrenArray(true);
+
+ usort($groups, function ($a, $b) {
+ return $a["gidnumber"] <=> $b["gidnumber"];
+ });
+
+ $id = self::ID_MAP[0];
+ foreach ($groups as $acc) {
+ if ($id == $acc["gidnumber"][0]) {
+ $id++;
+ if ($id > self::ID_MAP[1]) {
+ throw new Exception("GID Limits reached"); // all hell has broken if this executes
+ }
+ } else {
+ break;
+ }
+ }
+
+ return $id;
+ }
+
+ public function getNextPiGID()
+ {
+ $groups = $this->pi_groupOU->getChildrenArray(true);
+
+ usort($groups, function ($a, $b) {
+ return $a["gidnumber"] <=> $b["gidnumber"];
+ });
+
+ $id = self::PI_ID_MAP[0];
+ foreach ($groups as $acc) {
+ if ($id == $acc["gidnumber"][0]) {
+ $id++;
+ if ($id > self::PI_ID_MAP[1]) {
+ throw new Exception("Storage GID Limits reached"); // all hell has broken if this executes
+ }
+ } else {
+ break;
+ }
+ }
+
+ return $id;
+ }
+
+ public function getAllRecipients()
+ {
+ $users = $this->userOU->getChildren(true);
+
+ $out = array();
+ foreach ($users as $user) {
+ array_push($out, $user->getAttribute("mail")[0]);
+ }
+
+ return $out;
+ }
+
+ private function UIDNumInUse($id) {
+ $users = $this->userOU->getChildrenArray(true);
+ foreach ($users as $user) {
+ if ($user["uidnumber"][0] == $id) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function GIDNumInUse($id) {
+ $users = $this->groupOU->getChildrenArray(true);
+ foreach ($users as $user) {
+ if ($user["gidnumber"][0] == $id) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function getUnassignedID($uid)
+ {
+ $netid = strtok($uid, "_"); // extract netid
+ // scrape all files in custom folder
+ $dir = new DirectoryIterator(config::PATHS["custom_user"]);
+ foreach ($dir as $fileinfo) {
+ if ($fileinfo->getExtension() == "csv") {
+ // found csv file
+ $handle = fopen($fileinfo->getPathname(), "r");
+ while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
+ $netid_match = $data[0];
+ $uid_match = $data[1];
+
+ if ($uid == $netid_match || $netid == $netid_match) {
+ // found a match
+ if (!$this->UIDNumInUse($uid_match) && !$this->GIDNumInUse($uid_match)) {
+ return $uid_match;
+ }
+ }
+ }
+ }
+ }
+
+ // didn't find anything from existing mappings, use next available
+ $next_uid = $this->getNextUID();
+ if ($this->GIDNumInUse($next_uid)) {
+ throw new Exception("UID/GID Mismatch");
+ }
+
+ return $next_uid;
+ }
+
+ public function getAllUsers($services)
+ {
+ $users = $this->userOU->getChildren(true);
+
+ $out = array();
+ foreach ($users as $user) {
+ array_push($out, new unityUser($user->getAttribute("cn")[0], $services));
+ }
+
+ return $out;
+ }
+}
diff --git a/resources/libraries/unity-service.php b/resources/libraries/unity-service.php
new file mode 100644
index 00000000..3ba1a18a
--- /dev/null
+++ b/resources/libraries/unity-service.php
@@ -0,0 +1,170 @@
+ array(),
+ "sql" => array(),
+ "mail" => array(),
+ "unityfs" => array(),
+ "sacctmgr" => array()
+ );
+
+ public function __construct()
+ {
+ }
+
+ public function add_ldap($details, $name = self::DEFAULT_KEY)
+ {
+ if (array_key_exists($name, $this->services["ldap"])) {
+ throw new Exception("Service '$name' already exists.");
+ }
+
+ $ldap_object = new unityLDAP($details["uri"], $details["bind_dn"], $details["bind_pass"]);
+ $this->services["ldap"][$name] = $ldap_object;
+
+ return $this;
+ }
+
+ public function add_sql($details, $name = self::DEFAULT_KEY)
+ {
+ if (array_key_exists($name, $this->services["sql"])) {
+ throw new Exception("Service '$name' already exists.");
+ }
+
+ $sql_object = new unitySQL($details["host"], $details["db"], $details["user"], $details["pass"]);
+ $this->services["sql"][$name] = $sql_object;
+
+ return $this;
+ }
+
+ public function add_mail($details, $name = self::DEFAULT_KEY)
+ {
+ if (array_key_exists($name, $this->services["mail"])) {
+ throw new Exception("Service '$name' already exists.");
+ }
+
+ if (!array_key_exists("template_path", $details)) {
+ throw new Exception("Template path not set.");
+ }
+ $mailer = new templateMailer($details["template_path"]);
+
+ $mailer->isSMTP();
+ //$mailer->SMTPDebug = 4; // DEBUG
+
+ if (!array_key_exists("host", $details)) {
+ throw new Exception("Hostname not set.");
+ }
+ $mailer->Host = $details["host"];
+
+ if (!array_key_exists("port", $details)) {
+ throw new Exception("Port not set");
+ }
+ $mailer->Port = $details["port"];
+
+ if (!array_key_exists("smtp_options", $details)) {
+ $mailer->SMTPOptions = array(
+ 'ssl' => array(
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'allow_self_signed' => true
+ )
+ );
+ } else {
+ $mailer->SMTPOptions = $details["smtp_options"];
+ }
+
+ if (array_key_exists("smtp_secure", $details)) {
+ $mailer->SMTPSecure = $details["smtp_secure"];
+ }
+
+ if (array_key_exists("smtp_user", $details) && array_key_exists("smtp_pass", $details)) {
+ $mailer->SMTPAuth = true;
+ $mailer->Username = $details["smtp_user"];
+ $mailer->Password = $details["smtp_pass"];
+ } else {
+ $mailer->SMTPAuth = false;
+ }
+
+ $this->services["mail"][$name] = $mailer;
+
+ return $this;
+ }
+
+ public function add_sacctmgr($details, $name = self::DEFAULT_KEY)
+ {
+ if (array_key_exists($name, $this->services["sacctmgr"])) {
+ throw new Exception("Service '$name' already exists.");
+ }
+
+ if (!array_key_exists("cluster", $details)) {
+ throw new Exception("Slurm cluster name must be set.");
+ }
+
+ $sacctmgr = new slurm($details["cluster"]);
+
+ $this->services["sacctmgr"][$name] = $sacctmgr;
+
+ return $this;
+ }
+
+ public function add_unityfs($details, $name = self::DEFAULT_KEY)
+ {
+ if (array_key_exists($name, $this->services["unityfs"])) {
+ throw new Exception("Service '$name' already exists.");
+ }
+
+ $unityfs = new unityfs($details["host"], $details["port"]);
+
+ $this->services["unityfs"][$name] = $unityfs;
+
+ return $this;
+ }
+
+ public function ldap($name = self::DEFAULT_KEY)
+ {
+ if (array_key_exists($name, $this->services["ldap"])) {
+ return $this->services["ldap"][$name];
+ } else {
+ return NULL;
+ }
+ }
+
+ public function sql($name = self::DEFAULT_KEY)
+ {
+ if (array_key_exists($name, $this->services["sql"])) {
+ return $this->services["sql"][$name];
+ } else {
+ return NULL;
+ }
+ }
+
+ public function mail($name = self::DEFAULT_KEY)
+ {
+ if (array_key_exists($name, $this->services["mail"])) {
+ return $this->services["mail"][$name];
+ } else {
+ return NULL;
+ }
+ }
+
+ public function sacctmgr($name = self::DEFAULT_KEY)
+ {
+ if (array_key_exists($name, $this->services["sacctmgr"])) {
+ return $this->services["sacctmgr"][$name];
+ } else {
+ return NULL;
+ }
+ }
+
+ public function unityfs($name = self::DEFAULT_KEY)
+ {
+ if (array_key_exists($name, $this->services["unityfs"])) {
+ return $this->services["unityfs"][$name];
+ } else {
+ return NULL;
+ }
+ }
+}
diff --git a/resources/libraries/unity-sql.php b/resources/libraries/unity-sql.php
new file mode 100644
index 00000000..f65bb53c
--- /dev/null
+++ b/resources/libraries/unity-sql.php
@@ -0,0 +1,83 @@
+conn = new PDO("mysql:host=" . $db_host . ";dbname=" . $db, $db_user, $db_pass);
+ $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ }
+
+ public function getConn() {
+ return $this->conn;
+ }
+
+ //
+ // requests table methods
+ //
+ public function addRequest($requestor, $dest = self::REQUEST_ADMIN) {
+ if ($this->requestExists($requestor, $dest)) {
+ throw new Exception("This request already exists");
+ }
+
+ $stmt = $this->conn->prepare("INSERT INTO " . self::TABLE_REQS . " (uid, request_for) VALUES (:uid, :request_for)");
+ $stmt->bindParam(":uid", $requestor);
+ $stmt->bindParam(":request_for", $dest);
+
+ $stmt->execute();
+ }
+
+ public function removeRequest($requestor, $dest = self::REQUEST_ADMIN) {
+ if (!$this->requestExists($requestor, $dest)) {
+ throw new Exception("This request doesn't exist");
+ }
+
+ $stmt = $this->conn->prepare("DELETE FROM " . self::TABLE_REQS . " WHERE uid=:uid and request_for=:request_for");
+ $stmt->bindParam(":uid", $requestor);
+ $stmt->bindParam(":request_for", $dest);
+
+ $stmt->execute();
+ }
+
+ public function removeRequests($dest = self::REQUEST_ADMIN) {
+ $stmt = $this->conn->prepare("DELETE FROM " . self::TABLE_REQS . " WHERE request_for=:request_for");
+ $stmt->bindParam(":request_for", $dest);
+
+ $stmt->execute();
+ }
+
+ public function requestExists($requestor, $dest = self::REQUEST_ADMIN) {
+ $stmt = $this->conn->prepare("SELECT * FROM " . self::TABLE_REQS . " WHERE uid=:uid and request_for=:request_for");
+ $stmt->bindParam(":uid", $requestor);
+ $stmt->bindParam(":request_for", $dest);
+
+ $stmt->execute();
+
+ return count($stmt->fetchAll()) > 0;
+ }
+
+ public function getRequests($dest = self::REQUEST_ADMIN) {
+ $stmt = $this->conn->prepare("SELECT * FROM " . self::TABLE_REQS . " WHERE request_for=:request_for");
+ $stmt->bindParam(":request_for", $dest);
+
+ $stmt->execute();
+
+ return $stmt->fetchAll();
+ }
+
+ public function getRequestsByUser($user) {
+ $stmt = $this->conn->prepare("SELECT * FROM " . self::TABLE_REQS . " WHERE uid=:uid");
+ $stmt->bindParam(":uid", $user);
+
+ $stmt->execute();
+
+ return $stmt->fetchAll();
+ }
+}
diff --git a/resources/libraries/unity-user.php b/resources/libraries/unity-user.php
new file mode 100644
index 00000000..bcecb7c2
--- /dev/null
+++ b/resources/libraries/unity-user.php
@@ -0,0 +1,357 @@
+uid = $uid;
+
+ if (is_null($service_stack->ldap())) {
+ throw new Exception("LDAP is required for the unityUser class");
+ }
+
+ if (is_null($service_stack->sql())) {
+ throw new Exception("SQL is required for the unityUser class");
+ }
+
+ if (is_null($service_stack->sacctmgr())) {
+ throw new Exception("sacctmgr is required for the unityUser class");
+ }
+
+ if (is_null($service_stack->unityfs())) {
+ throw new Exception("unityfs is required for the unityUser class");
+ }
+
+ $this->service_stack = $service_stack;
+ }
+
+ /**
+ * This is the method that is run when a new account is created
+ *
+ * @param string $firstname First name of new account
+ * @param string $lastname Last name of new account
+ * @param string $email email of new account
+ * @param bool $isPI boolean value for if the user checked the "I am a PI box"
+ * @return void
+ */
+ public function init($firstname, $lastname, $email)
+ {
+ //
+ // Create LDAP group
+ //
+ $ldapGroupEntry = $this->getLDAPGroup();
+ $id = $this->service_stack->ldap()->getUnassignedID($this->getUID());
+
+ if (!$ldapGroupEntry->exists()) {
+ $ldapGroupEntry->setAttribute("objectclass", unityLDAP::POSIX_GROUP_CLASS);
+ $ldapGroupEntry->setAttribute("gidnumber", strval($id));
+
+ if (!$ldapGroupEntry->write()) {
+ throw new Exception("Failed to create POSIX group for $this->uid");
+ }
+ }
+
+ //
+ // Create LDAP user
+ //
+ $ldapUserEntry = $this->getLDAPUser();
+
+ if (!$ldapUserEntry->exists()) {
+ $ldapUserEntry->setAttribute("objectclass", unityLDAP::POSIX_ACCOUNT_CLASS);
+ $ldapUserEntry->setAttribute("uid", $this->uid);
+ $ldapUserEntry->setAttribute("givenname", $firstname);
+ $ldapUserEntry->setAttribute("sn", $lastname);
+ $ldapUserEntry->setAttribute("mail", $email);
+ $ldapUserEntry->setAttribute("homedirectory", self::HOME_DIR . $this->uid);
+ $ldapUserEntry->setAttribute("loginshell", unityLDAP::DEFAULT_SHELL);
+ $ldapUserEntry->setAttribute("uidnumber", strval($id));
+ $ldapUserEntry->setAttribute("gidnumber", strval($id));
+
+ if (!$ldapUserEntry->write()) {
+ $ldapGroupEntry->delete(); // Cleanup previous group
+ throw new Exception("Failed to create POSIX user for $this->uid");
+ }
+ }
+
+ //
+ // MySQL row
+ //
+ if ($isPI) {
+ $this->service_stack->sql()->addRequest($this->uid);
+ }
+
+ // filesystem
+ $this->initFilesystem();
+ }
+
+
+ /**
+ * Returns the ldap account entry corresponding to the user
+ *
+ * @return ldapEntry posix account
+ */
+ public function getLDAPUser()
+ {
+ $user_entries = $this->service_stack->ldap()->userOU->getChildren(true, "(" . unityLDAP::RDN . "=$this->uid)");
+
+ if (count($user_entries) > 0) {
+ return $user_entries[0];
+ } else {
+ return new ldapEntry($this->service_stack->ldap()->getConn(), unityLDAP::RDN . "=$this->uid," . unityLDAP::USERS);
+ }
+ }
+
+ /**
+ * Returns the ldap group entry corresponding to the user
+ *
+ * @return ldapEntry posix group
+ */
+ public function getLDAPGroup()
+ {
+ $group_entries = $this->service_stack->ldap()->groupOU->getChildren(true, "(" . unityLDAP::RDN . "=$this->uid)");
+
+ if (count($group_entries) > 0) {
+ return $group_entries[0];
+ } else {
+ return new ldapEntry($this->service_stack->ldap()->getConn(), unityLDAP::RDN . "=$this->uid," . unityLDAP::GROUPS);
+ }
+ }
+
+ public function exists()
+ {
+ return $this->getLDAPUser()->exists() && $this->getLDAPGroup()->exists();
+ }
+
+ //
+ // User Attribute Functions
+ //
+
+ /**
+ * Get method for NetID
+ *
+ * @return string Net ID of user
+ */
+ public function getUID()
+ {
+ return $this->uid;
+ }
+
+ /**
+ * Sets the firstname of the account and the corresponding ldap entry if it exists
+ *
+ * @param string $firstname
+ */
+ public function setFirstname($firstname)
+ {
+ $this->getLDAPUser()->setAttribute("givenname", $firstname);
+
+ if (!$this->getLDAPUser()->write()) {
+ throw new Exception("Error updating LDAP entry $this->uid");
+ }
+ }
+
+ /**
+ * Gets the firstname of the account
+ *
+ * @return string firstname
+ */
+ public function getFirstname()
+ {
+ return $this->getLDAPUser()->getAttribute("givenname")[0];
+ }
+
+ /**
+ * Sets the lastname of the account and the corresponding ldap entry if it exists
+ *
+ * @param string $lastname
+ */
+ public function setLastname($lastname)
+ {
+ $this->getLDAPUser()->setAttribute("sn", $lastname);
+
+ if (!$this->getLDAPUser()->write()) {
+ throw new Exception("Error updating LDAP entry $this->uid");
+ }
+ }
+
+ /**
+ * Get method for the lastname on the account
+ *
+ * @return string lastname
+ */
+ public function getLastname()
+ {
+ return $this->getLDAPUser()->getAttribute("sn")[0];
+ }
+
+ public function getFullname() {
+ return $this->getFirstname() . " " . $this->getLastname();
+ }
+
+ /**
+ * Sets the mail in the account and the ldap entry
+ *
+ * @param string $mail
+ */
+ public function setMail($email)
+ {
+ $this->getLDAPUser()->setAttribute("mail", $email);
+
+ if (!$this->getLDAPUser()->write()) {
+ throw new Exception("Error updating LDAP entry $this->uid");
+ }
+ }
+
+ /**
+ * Method to get the mail instance var
+ *
+ * @return string email address
+ */
+ public function getMail()
+ {
+ return $this->getLDAPUser()->getAttribute("mail")[0];
+ }
+
+ /**
+ * Sets the SSH keys on the account and the corresponding entry
+ *
+ * @param array $keys String array of openssh-style ssh public keys
+ */
+ public function setSSHKeys($keys)
+ {
+ $ldapUser = $this->getLDAPUser();
+ $keys_filt = array_values(array_unique($keys));
+ if ($ldapUser->exists()) {
+ $ldapUser->setAttribute("sshpublickey", $keys_filt);
+ if (!$ldapUser->write()) {
+ throw new Exception("Failed to modify SSH keys for $this->uid");
+ }
+ }
+ }
+
+ /**
+ * Returns the SSH keys attached to the account
+ *
+ * @return array String array of ssh keys
+ */
+ public function getSSHKeys()
+ {
+ $ldapUser = $this->getLDAPUser();
+ $result = $ldapUser->getAttribute("sshpublickey");
+ if (is_null($result)) {
+ return array();
+ } else {
+ return $result;
+ }
+ }
+
+ /**
+ * Sets the login shell for the account
+ *
+ * @param string $shell absolute path to shell
+ */
+ public function setLoginShell($shell)
+ {
+ $ldapUser = $this->getLDAPUser();
+ if ($ldapUser->exists()) {
+ $ldapUser->setAttribute("loginshell", $shell);
+ if (!$ldapUser->write()) {
+ throw new Exception("Failed to modify login shell for $this->uid");
+ }
+ }
+ }
+
+ /**
+ * Gets the login shell of the account
+ *
+ * @return string absolute path to login shell
+ */
+ public function getLoginShell()
+ {
+ $ldapUser = $this->getLDAPUser();
+ return $ldapUser->getAttribute("loginshell")[0];
+ }
+
+ public function setHomeDir($home)
+ {
+ $ldapUser = $this->getLDAPUser();
+ if ($ldapUser->exists()) {
+ $ldapUser->setAttribute("homedirectory", $home);
+ if (!$ldapUser->write()) {
+ throw new Exception("Failed to modify home directory for $this->uid");
+ }
+ }
+ }
+
+ /**
+ * Gets the home directory of the user, the home directory is immutable
+ *
+ * @return string path to home directory
+ */
+ public function getHomeDir()
+ {
+ return self::HOME_DIR . $this->netid;
+ }
+
+ /**
+ * Checks if the current account is an admin (in the sudo group)
+ *
+ * @return boolean true if admin, false if not
+ */
+ public function isAdmin()
+ {
+ $admins = $this->service_stack->ldap()->adminGroup->getAttribute("memberuid");
+ return in_array($this->uid, $admins);
+ }
+
+ /**
+ * Checks if current user is a PI
+ *
+ * @return boolean true is PI, false if not
+ */
+ public function isPI()
+ {
+ return $this->getAccount()->exists();
+ }
+
+ public function getAccount()
+ {
+ return new unityAccount(unityAccount::getPIUIDfromUID($this->uid), $this->service_stack);
+ }
+
+ /**
+ * Gets the groups this user is assigned to, can be more than one
+ * @return [type]
+ */
+ public function getGroups()
+ {
+ $groups = $this->service_stack->sacctmgr()->getAccountsFromUser($this->uid);
+
+ $out = array();
+ foreach ($groups as $group) {
+ array_push($out, new unityAccount($group, $this->service_stack));
+ }
+ return $out;
+ }
+
+ public function initFilesystem() {
+ $this->service_stack->unityfs()->createHomeDirectory($this->getUID(), self::HOME_QUOTA);
+ $this->service_stack->unityfs()->createScratchDirectory($this->getUID());
+ $this->service_stack->unityfs()->populateHomeDirectory($this->getUID());
+ $this->service_stack->unityfs()->populateScratchDirectory($this->getUID());
+ }
+}
diff --git a/resources/libraries/unityfs.php b/resources/libraries/unityfs.php
new file mode 100644
index 00000000..225ae28c
--- /dev/null
+++ b/resources/libraries/unityfs.php
@@ -0,0 +1,74 @@
+s = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+ socket_set_option($this->s, SOL_SOCKET, SO_REUSEADDR, 1);
+ if (!socket_connect($this->s, $host, $port)) {
+ throw new Exception("Unable to connect to socket");
+ }
+ }
+
+ public function createHomeDirectory($uid, $quota) {
+ $request = "";
+ $request .= "$uid$uid";
+ $request .= "$quota";
+ $request .= "";
+
+ if (!socket_send($this->s, $request, strlen($request), 0)) {
+ throw new Exception("Could not send data to unityfs");
+ }
+
+ $result = socket_read($this->s, 1024);
+ return $result;
+ }
+
+ public function populateHomeDirectory($uid) {
+ $request = "";
+ $request .= "/home/$uid";
+ $request .= "$uid$uid";
+ $request .= "/scratch/$uid";
+ $request .= "";
+
+ if (!socket_send($this->s, $request, strlen($request), 0)) {
+ throw new Exception("Could not send data to unityfs");
+ }
+
+ $result = socket_read($this->s, 1024);
+ return $result;
+ }
+
+ public function createScratchDirectory($uid) {
+ $request = "";
+ $request .= "$uid$uid";
+ $request .= "";
+
+ if (!socket_send($this->s, $request, strlen($request), 0)) {
+ throw new Exception("Could not send data to unityfs");
+ }
+
+ $result = socket_read($this->s, 1024);
+ return $result;
+ }
+
+ public function populateScratchDirectory($uid) {
+ $request = "";
+ $request .= "/scratch/$uid";
+ $request .= "$uid$uid";
+ $request .= "";
+
+ if (!socket_send($this->s, $request, strlen($request), 0)) {
+ throw new Exception("Could not send data to unityfs");
+ }
+
+ $result = socket_read($this->s, 1024);
+ return $result;
+ }
+
+ public function close() {
+ socket_close($this->s);
+ }
+}
\ No newline at end of file
diff --git a/resources/locale/en.json b/resources/locale/en.json
new file mode 100644
index 00000000..3328dffe
--- /dev/null
+++ b/resources/locale/en.json
@@ -0,0 +1,5 @@
+{
+ "globals": {
+
+ }
+}
\ No newline at end of file
diff --git a/resources/locale/en.php b/resources/locale/en.php
new file mode 100644
index 00000000..e2e2f65e
--- /dev/null
+++ b/resources/locale/en.php
@@ -0,0 +1,164 @@
+__
+ * type = err,warn,mes,label,header
+ */
+class unity_locale {
+ //
+ // General
+ //
+ public const ERR = "An error occured, please contact the admins.";
+ public const LABEL_NAME = "Name";
+ public const LABEL_USERID = "User ID";
+ public const LABEL_ACTIONS = "Actions";
+ public const LABEL_MAIL = "Email";
+ public const LABEL_SUBJECT = "Subject";
+ public const LABEL_MESSAGE = "Message";
+ public const LABEL_APPLY = "Apply Changes";
+ public const MES_APPLY = "Changes Applied";
+
+ public const LABEL_ERROR = "Error:";
+ public const LABEL_SUCCESS = "Success:";
+
+ //
+ // Home (/index.php)
+ //
+ public const HOME_HEADER_MAIN = "Unity Cluster";
+
+ //
+ // Authentication (/panel-auth.php)
+ //
+ public const AUTH_HEADER_MAIN = "Campus Authentication";
+
+ //
+ // AUP/Priv (/priv.php)
+ //
+ public const PRIV_HEADER_PRIVACY = "Privacy Policy";
+ public const PRIV_HEADER_AUP = "Acceptable User Policy";
+
+ //
+ // About (/about.php)
+ //
+ public const ABOUT_HEADER_MAIN = "About Us";
+
+ //
+ // Contact Us (/contact.php)
+ //
+ public const CONTACT_HEADER_MAIN = "Contact Us";
+ public const CONTACT_ERR_NAME = "Enter a name";
+ public const CONTACT_ERR_MAIL = "Enter a valid email";
+ public const CONTACT_ERR_SUBJECT = "Enter a subject";
+ public const CONTACT_ERR_MESSAGE = "Enter a message";
+ public const CONTACT_LABEL_SEND = "Send Message";
+ public const CONTACT_MES_SENT = "Message sent";
+ public const CONTACT_WARN_SEND = "Are you sure you would like to send this message?";
+
+ //
+ // Cluster Status (/cluster-status.php)
+ //
+ public const CLUSTER_HEADER_MAIN = "Cluster Status";
+ public const CLUSTER_LABEL_USAGE = "Total CPU Usage";
+ public const CLUSTER_LABEL_UP = "UP";
+ public const CLUSTER_LABEL_DOWN = "DOWN";
+
+ //
+ // Panel Home (/panel/index.php)
+ //
+ public const PANEL_HEADER_MAIN = "User Panel";
+
+ //
+ // New Account (/panel/new_account.php)
+ //
+ public const NEWACC_HEADER_MAIN = "Request Account";
+ public const NEWACC_LABEL_SELECTPI = " -- Select Principal Investigator -- ";
+ public const NEWACC_LABEL_EXISTINGPI = " -- Existing PI --";
+ public const NEWACC_ERR_PI = "Select a valid PI, or select \"New PI\"";
+ public const NEWACC_LABEL_ARCHIVAL = "You are an archived account. This could be due to you closing your account, or your account being removed. Please select a PI to request to restore your account. Your files are archived and will be restored upon approval.";
+ public const NEWACC_LABEL_AWAITING = "Your account request has been submitted but it has not yet been approved. You will not be able to submit this form.";
+ public const NEWACC_LABEL_REQUEST = "Request Account";
+ public const NEWACC_MES_SUCCESS = "Your account request has been submitted. You will receive email confirmation once your account is created.";
+
+ //
+ // Account Settings (/panel/account.php)
+ //
+ public const ACCOUNT_HEADER_MAIN = "Account Settings";
+ public const ACCOUNT_LABEL_NEW = "Add New Key";
+ public const ACCOUNT_LABEL_GENERATE = "Generate Key Pair";
+ public const ACCOUNT_LABEL_GENWIN = "Windows (PuTTY)";
+ public const ACCOUNT_LABEL_GENLIN = "Linux / Max (OpenSSH)";
+ public const ACCOUNT_LABEL_REMKEY = "Remove SSH Key";
+ public const ACCOUNT_LABEL_KEY = "ssh-rsa AAAAB3Nza...";
+
+ //
+ // My Groups (/panel/groups.php)
+ //
+ public const GROUP_HEADER_MAIN = "My Groups";
+ public const GROUP_BTN_JOIN_PI = "Join PI";
+ public const GROUP_BTN_ACTIVATE_PI = "Activate PI Account";
+ public const GROUP_WARN_ACTIVATE_PI = "Are you sure you want to activate your PI account? You need to be a PI at your institution for this request.";
+
+ public static function GROUP_WARN_REMOVE($name) {
+ return "Are you sure you would like to leave the PI group $name?";
+ }
+
+ //
+ // Mass Email (/admin/mass_email.php)
+ //
+ public const MASS_HEADER_MAIN = "Mass Email";
+ public const MASS_ERR_SUBJECT = "Enter a valid subject";
+ public const MASS_ERR_MESSAGE = "Enter a valid message";
+ public const MASS_WARN_SEND = "Are you sure you would like to send this mass email?";
+ public const MASS_LABEL_SEND = "Send Mass Email";
+ public const MASS_MES_SEND = "Mass email was sent successfully";
+
+ //
+ // User Management (/admin/user-mgmt.php)
+ //
+ public const USER_HEADER_MAIN = "User Management";
+
+ //
+ // Mail Templates
+ //
+ public static function MAIL_LABEL_FOOTER($url) {
+ return "You are receiving this email because you have an account on the Unity Cluster at UMass Amherst. If you would like to stop receiving these emails, you may request to close your account by replying to this email.";
+ }
+
+ public const MAIL_HEADER_PIREQUEST = "New User Request";
+ public const MAIL_LABEL_PIREQUEST = "A user has requested an account in your PI group:";
+
+ public const MAIL_HEADER_ADREQUEST = "New PI Request";
+ public const MAIL_LABEL_ADREQUEST = "A user has requested a new PI account:";
+
+ public const MAIL_HEADER_PIJOIN = "PI Request Approved";
+ public static function MAIL_LABEL_PIJOIN($group) {
+ return "Your request to join the PI group $group has been approved. You may view this group in in the 'My Groups' page on the user portal";
+ }
+
+ public const MAIL_HEADER_PIDENY = "PI Request Denied";
+ public static function MAIL_LABEL_PIDENY($group) {
+ return "Your request to join the PI group $group has been denied. You may follow up with the group owner if necessary.";
+ }
+
+ public const MAIL_HEADER_PIREM = "Removed from Group";
+ public static function MAIL_LABEL_PIREM($group) {
+ return "You have been removed from the PI group $group. Your files have not been removed, only your association with $group. Feel free to reply with any questions.";
+ }
+
+ public const MAIL_HEADER_ADMIN_APP_PI = "PI Account Approved";
+ public const MAIL_LABEL_ADMIN_APP_PI = "Your request to create a PI account has been approved. You can manage your group on the 'PI Management' page in the user portal.";
+
+ public const MAIL_HEADER_ADMIN_DENY_PI = "PI Account Denied";
+ public const MAIL_LABEL_ADMIN_DENY_PI = "Your request to create a PI account has been denied. This could be for a number of reasons. Most comonly, you may not be a principal investigator at your institution. Feel free to reply to this email with any questions.";
+
+ public const MAIL_HEADER_ADMIN_DISBAND_PI = "PI Account Disbanded";
+ public const MAIL_LABEL_ADMIN_DISBAND_PI = "Your PI group has been disbanded. This means you and all users part of the group no longer have an association with it. Your user accounts and files are still intact, as they are seperate from PI accounts";
+
+ public const MAIL_HEADER_LEFT_PI = "User Left PI Group";
+ public const MAIL_LABEL_LEFT_PI = "A user has left your PI group, details below:";
+
+ public static function MAIL_MES_ACTIVATE($url) {
+ return "You can approve this account here.";
+ }
+}
\ No newline at end of file
diff --git a/resources/templates/footer.php b/resources/templates/footer.php
new file mode 100644
index 00000000..c74ccd82
--- /dev/null
+++ b/resources/templates/footer.php
@@ -0,0 +1,18 @@
+
+
+
+
+