Skip to content

Research User Architecture Design

Overview

Prism's multi-user architecture implements a one-user-per-instance model where each instance contains: 1. System User: For system administration and service management 2. Research User: For actual research work, connected to the researcher's identity

Research users are per-profile, meaning each AWS account invitation creates a unique research user identity with consistent UID/GID mapping across all instances in that account.

Core Principles

Single-User Instance Model

  • One research user per instance - Prism extends your laptop/workstation concept
  • No multi-tenancy - Each instance is dedicated to one researcher
  • Profile-Based Identity - Research user tied to profile (AWS account), not global
  • Consistent Identity - Same UID/GID across all instances in an AWS account

Identity Integration

  • Optional Globus Auth - Enhanced identity verification for sensitive invitations
  • Device Binding - Secure device-based authorization (existing system)
  • Profile Isolation - Research users cannot cross AWS account boundaries

Architecture Design

Research User Model

// ResearchUser represents the research identity for a profile
type ResearchUser struct {
    // Core identity (per-profile, not global)
    ProfileID    string `json:"profile_id"`    // Links to Profile.AWSProfile
    Username     string `json:"username"`      // Research username (e.g., "alice_researcher")
    UID          int    `json:"uid"`           // Consistent UID across all instances
    GID          int    `json:"gid"`           // Consistent GID across all instances

    // Identity verification
    GlobusIdentity *GlobusIdentity `json:"globus_identity,omitempty"` // Optional Globus Auth
    EmailVerified  bool            `json:"email_verified"`            // Email verification status

    // SSH key management (per-profile)
    SSHPublicKey  string `json:"ssh_public_key,omitempty"`  // Research user SSH key
    SSHKeyPath    string `json:"ssh_key_path,omitempty"`    // Local private key path

    // Research environment preferences
    Shell         string            `json:"shell,omitempty"`         // Default: /bin/bash
    HomeDirMount  string           `json:"home_dir_mount,omitempty"` // EFS mount for home directory
    Groups        []string         `json:"groups,omitempty"`         // Additional groups (docker, etc.)

    // Metadata
    CreatedAt     time.Time `json:"created_at"`
    LastUsed      time.Time `json:"last_used"`
    DisplayName   string    `json:"display_name,omitempty"` // From invitation or Globus
}

// GlobusIdentity represents optional Globus Auth integration
type GlobusIdentity struct {
    GlobusID     string `json:"globus_id"`      // Globus user identifier
    ORCID        string `json:"orcid,omitempty"` // ORCID if linked
    Institution  string `json:"institution,omitempty"` // Institution from Globus
    Email        string `json:"email,omitempty"` // Verified email from Globus
    VerifiedAt   time.Time `json:"verified_at"` // When Globus verification completed
}

Profile Integration

Enhanced profile structure with research user:

// Extension to existing Profile struct
type Profile struct {
    // ... existing fields ...

    // Research user identity (new fields)
    ResearchUser   *ResearchUser `json:"research_user,omitempty"`   // Research identity
    RequireGlobus  bool          `json:"require_globus,omitempty"`  // Globus Auth required

    // Home directory management
    EFSHomeDir     string        `json:"efs_home_dir,omitempty"`    // EFS filesystem for home
    HomeDirectory  string        `json:"home_directory,omitempty"`  // Home directory path
}

Invitation Enhancement

Enhanced invitation system with optional Globus requirements:

// Extension to existing InvitationToken
type InvitationToken struct {
    // ... existing fields ...

    // Research user requirements
    RequireGlobusAuth    bool     `json:"require_globus_auth,omitempty"`  // Globus Auth required
    AllowedInstitutions  []string `json:"allowed_institutions,omitempty"` // Institution whitelist
    RequiredORCID        bool     `json:"required_orcid,omitempty"`       // ORCID required

    // Research user provisioning
    DefaultUsername      string   `json:"default_username,omitempty"`     // Suggested username
    DefaultShell         string   `json:"default_shell,omitempty"`        // Default shell
    AdditionalGroups     []string `json:"additional_groups,omitempty"`    // Extra groups
}

Implementation Flow

Invitation Acceptance with Research User Creation

sequenceDiagram
    participant User
    participant CLI
    participant InvitationManager
    participant GlobusAuth
    participant ProfileManager
    participant AWSManager

    User->>CLI: prism profiles accept-invitation --encoded [token]
    CLI->>InvitationManager: DecodeInvitation(token)
    InvitationManager->>CLI: invitation

    alt Globus Auth Required
        CLI->>User: Globus Auth required - opening browser
        CLI->>GlobusAuth: InitiateOAuthFlow()
        GlobusAuth->>User: Browser OAuth flow
        User->>GlobusAuth: Login & consent
        GlobusAuth->>CLI: AuthCode
        CLI->>GlobusAuth: ExchangeToken(authCode)
        GlobusAuth->>CLI: GlobusIdentity
    end

    CLI->>ProfileManager: CreateResearchUser(invitation, globusIdentity)
    ProfileManager->>ProfileManager: GenerateUID/GID()
    ProfileManager->>ProfileManager: CreateProfile()
    ProfileManager->>AWSManager: CreateEFSHomeDirectory()
    ProfileManager->>CLI: profile created
    CLI->>User: Profile ready, research user configured

Instance Launch with Research User

sequenceDiagram
    participant User
    participant CLI
    participant TemplateResolver
    participant AWSManager
    participant Instance

    User->>CLI: prism workspace launch python-ml my-project
    CLI->>TemplateResolver: ResolveTemplate() with profile
    TemplateResolver->>TemplateResolver: GenerateUserData() for research user
    CLI->>AWSManager: LaunchInstance() with enhanced user data

    AWSManager->>Instance: EC2 Instance with user data
    Instance->>Instance: Create system user (ubuntu/ec2-user)
    Instance->>Instance: Create research user (UID/GID from profile)
    Instance->>Instance: Mount EFS home directory
    Instance->>Instance: Configure SSH keys for research user
    Instance->>Instance: Install packages as system, configure for research user
    Instance->>CLI: Instance ready
    CLI->>User: Connect via: ssh research_user@ip

Globus Auth Integration

OAuth 2.0 Flow

// GlobusAuthClient handles OAuth integration
type GlobusAuthClient struct {
    ClientID     string
    ClientSecret string
    RedirectURI  string
    Scopes       []string // ["openid", "profile", "email", "urn:globus:auth:scope:auth.globus.org:view_identities"]
}

// InitiateAuth starts Globus OAuth flow
func (g *GlobusAuthClient) InitiateAuth() (*AuthSession, error) {
    // Generate state parameter for CSRF protection
    state := generateSecureState()

    authURL := fmt.Sprintf(
        "https://auth.globus.org/v2/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&state=%s",
        g.ClientID,
        url.QueryEscape(g.RedirectURI),
        url.QueryEscape(strings.Join(g.Scopes, " ")),
        state,
    )

    return &AuthSession{
        AuthURL: authURL,
        State:   state,
    }, nil
}

// ExchangeAuthCode exchanges authorization code for tokens
func (g *GlobusAuthClient) ExchangeAuthCode(code, state string, session *AuthSession) (*GlobusIdentity, error) {
    // Verify state parameter
    if state != session.State {
        return nil, fmt.Errorf("state mismatch - possible CSRF attack")
    }

    // Exchange authorization code for access token
    tokenResp, err := g.exchangeCode(code)
    if err != nil {
        return nil, err
    }

    // Get user info from Globus Auth
    userInfo, err := g.getUserInfo(tokenResp.AccessToken)
    if err != nil {
        return nil, err
    }

    return &GlobusIdentity{
        GlobusID:    userInfo.Sub,
        Email:       userInfo.Email,
        Institution: userInfo.Organization,
        ORCID:       userInfo.ORCID,
        VerifiedAt:  time.Now(),
    }, nil
}

CLI Integration

# Enhanced invitation acceptance with optional Globus Auth
$ prism profiles accept-invitation --encoded [token] --name "Lab Collaboration"

# If invitation requires Globus Auth:
Invitation requires Globus Auth for identity verification.
Opening browser for authentication...

Globus Auth completed successfully:
  - Identity: alice.researcher@university.edu  
  - Institution: University of Research
  - ORCID: 0000-0000-0000-0000

Creating profile 'Lab Collaboration'...
   Research user: alice_researcher (UID: 5001)
   Home directory: EFS mount configured
   SSH key: Generated and configured
   Profile: Ready for use

Profile 'Lab Collaboration' created successfully.
Switch to it with: prism profiles switch Lab-Collaboration

Research User Provisioning

User Data Script Enhancement

Enhanced user data generation for research user creation:

#!/bin/bash
# Enhanced Prism user data with research user

# Create research user with consistent UID/GID
RESEARCH_USER="alice_researcher"
RESEARCH_UID=5001
RESEARCH_GID=5001

# Create research user
groupadd -g $RESEARCH_GID $RESEARCH_USER
useradd -u $RESEARCH_UID -g $RESEARCH_GID -m -s /bin/bash $RESEARCH_USER
usermod -aG sudo,docker $RESEARCH_USER

# Configure SSH access for research user
mkdir -p /home/$RESEARCH_USER/.ssh
echo "ssh-rsa AAAA... alice_researcher@prism" > /home/$RESEARCH_USER/.ssh/authorized_keys
chmod 600 /home/$RESEARCH_USER/.ssh/authorized_keys
chown -R $RESEARCH_USER:$RESEARCH_USER /home/$RESEARCH_USER/.ssh

# Mount EFS home directory (if configured)
if [ "$EFS_HOME_DIR" != "" ]; then
    mkdir -p /home/$RESEARCH_USER/workspace
    echo "$EFS_HOME_DIR.efs.us-west-2.amazonaws.com:/ /home/$RESEARCH_USER/workspace nfs4 defaults,_netdev" >> /etc/fstab
    mount /home/$RESEARCH_USER/workspace
    chown $RESEARCH_USER:$RESEARCH_USER /home/$RESEARCH_USER/workspace
fi

# Install packages as system user, configure for research user
apt-get update && apt-get install -y python3-pip jupyter-notebook

# Configure Jupyter for research user
sudo -u $RESEARCH_USER bash -c "
cd /home/$RESEARCH_USER
jupyter notebook --generate-config
echo \"c.NotebookApp.ip = '0.0.0.0'\" >> ~/.jupyter/jupyter_notebook_config.py
echo \"c.NotebookApp.open_browser = False\" >> ~/.jupyter/jupyter_notebook_config.py
"

# Create systemd service for Jupyter (runs as research user)
cat > /etc/systemd/system/jupyter-$RESEARCH_USER.service << EOF
[Unit]
Description=Jupyter Notebook for $RESEARCH_USER
After=network.target

[Service]
Type=simple
User=$RESEARCH_USER
WorkingDirectory=/home/$RESEARCH_USER
ExecStart=/usr/local/bin/jupyter notebook
Restart=always

[Install]
WantedBy=multi-user.target
EOF

systemctl enable jupyter-$RESEARCH_USER
systemctl start jupyter-$RESEARCH_USER

Security & Isolation

Per-Profile Isolation

  • UID/GID Uniqueness: Each AWS account gets unique UID/GID range (5000-5999, 6000-6999, etc.)
  • No Cross-Account Access: Research users cannot access other AWS accounts' resources
  • Device Binding: Existing security system maintains device restrictions
  • SSH Key Isolation: Each profile has separate SSH key pair

Optional Globus Enhancement

  • Identity Verification: Stronger identity assurance beyond email verification
  • Institution Validation: Ensure users belong to expected institutions
  • ORCID Integration: Link research identity to ORCID for academic workflows
  • Audit Trail: Complete audit of identity verification for compliance

This architecture provides a clean separation between system administration and research work while maintaining the simplicity of one-user-per-instance and optional enhanced identity verification through Globus Auth.