惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
博客园 - Franky
罗磊的独立博客
G
Google Developers Blog
The GitHub Blog
The GitHub Blog
P
Proofpoint News Feed
Recent Announcements
Recent Announcements
V
V2EX
F
Fortinet All Blogs
阮一峰的网络日志
阮一峰的网络日志
Blog — PlanetScale
Blog — PlanetScale
月光博客
月光博客
U
Unit 42
GbyAI
GbyAI
A
About on SuperTechFans
WordPress大学
WordPress大学
Engineering at Meta
Engineering at Meta
雷峰网
雷峰网
Microsoft Azure Blog
Microsoft Azure Blog
Martin Fowler
Martin Fowler
D
DataBreaches.Net
The Cloudflare Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MongoDB | Blog
MongoDB | Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building a Morpheus Plugin A Practical Walkthrough
Emre Baykal · 2026-04-30 · via DEV Community

Morpheus is a hybrid cloud management platform with a plugin SDK that lets you extend almost everything cloud integrations, custom reports, IPAM and DNS providers, backup integrations, UI tabs, global UI hooks. The SDK is documented in pieces, but the day-to-day workflow of going from "I have an idea" to "shadow JAR is running on my appliance" isn't laid out anywhere. This post walks through that workflow.

I'll use a plugin we recently built as the running example: a small enforcement utility that forces every Morpheus user to enable 2FA. If MFA isn't your problem, the example is incidental. What matters is the path.

What you can extend

The SDK exposes more than fifty provider types. A provider is your hook into Morpheus; Morpheus calls back into your code at well defined moments, and you pick the type that matches what you want to do.

A non exhaustive sample, mostly to give a feel for the catalog:

  • CloudProvider, ProvisionProvider — add a new cloud integration
  • BackupProvider — register a backup integration
  • ReportProvider — add a custom report type
  • AppTabProvider, ServerTabProvider, ClusterTabProvider, InstanceTabProvider, NetworkTabProvider — add a tab to a detail page
  • GlobalUIComponentProvider — inject HTML into every authenticated page render
  • TaskProvider — register a task type for the automation engine
  • CypherProvider — extend the secure-vault subsystem
  • GuidanceRecommendationProvider, AnalyticsProvider — feed dashboard widgets
  • GenericProvider — generic integration for things that don't fit elsewhere

The full list lives at morpheus-plugin-core/morpheus-plugin-api/src/main/java/com/morpheusdata/core/providers/. Picking the right one is the most important early decision; everything else flows from it.

Set up your reference material first

Before writing any code, clone these three repositories locally:

git clone --depth 1 --branch v1.2.x \
    https://github.com/HewlettPackard/morpheus-plugin-core.git
git clone --depth 1 \
    https://github.com/HewlettPackard/morpheus-docs.git
git clone --depth 1 \
    https://github.com/HewlettPackard/morpheus-openapi.git

Enter fullscreen mode Exit fullscreen mode

morpheus-plugin-core is the source of the com.morpheusdata:morpheus-plugin-api library you'll compile against. Provider interfaces, abstract classes, model classes everything is here. When you want to know what a method returns or which fields a model exposes, grep this.

morpheus-docs is the source of the official Morpheus product documentation, currently published as HPE Morpheus VM Essentials on the HPE support portal. When you need to understand a feature from the user's perspective what does Morpheus call this concept, how does the UI organize it this is the reference.

morpheus-openapi is the OpenAPI 3.1 spec for Morpheus's REST API: 600+ endpoint files, 650+ schemas. If your plugin needs to call Morpheus over HTTP, or you need the canonical shape of a data model, this is faster than the live docs site.

Grep is fast; docs search is slow. A local clone pays off within the first hour.

A few web resources are worth bookmarking alongside the local clones:

Use them when the local clones don't have what you need; the local clones answer most questions faster.

Pick the right provider type

The methodology is straightforward:

  1. Write down what you want Morpheus to do.
  2. Translate that into a question: when does Morpheus need to call my code?
  3. Find the provider whose method signatures answer that question.

For the MFA enforcer, the chain went:

"Force users without 2FA to enable it before they can use Morpheus."

"We need to be invoked on every page render, with the current user, so we can decide whether to render a blocking overlay."

Match: GlobalUIComponentProvider, which defines Boolean show(User, Account) and HTMLResponse renderTemplate(User, Account). Morpheus invokes both on every authenticated page render.

If we'd wanted a custom report, we'd have picked ReportProvider. If we'd wanted a tab on the instance detail page, InstanceTabProvider. The question-to-provider mapping becomes mechanical once you know the catalog.

One thing worth knowing up front: not every hook you might want exists. There is no LoginProvider and no AuthenticationProvider — you can't intercept the login flow itself. For the MFA enforcer that meant settling for GlobalUIComponentProvider, which runs after login. The SDK won't always give you exactly what you want; pick the closest match and design around its constraints.

Build environment

The plugin API targets Java 8, but Gradle 7.x needs JDK 11 to build. The shadow plugin used by older Morpheus samples (com.bmuschko.gradle-plugin-shadow 2.0.3) is Gradle 6.x only — use the maintained com.github.johnrengelman.shadow 7.1.2 instead.

On Linux:

curl -fsSL "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"

sdk install java 11.0.22-tem
sdk install gradle 7.6.4

cd my-plugin
gradle wrapper --gradle-version 7.6.4 --distribution-type bin
./gradlew shadowJar

Enter fullscreen mode Exit fullscreen mode

A minimal build.gradle:

plugins {
    id 'java'
    id 'groovy'
    id 'com.github.johnrengelman.shadow' version '7.1.2'
}

group   = 'com.morpheusdata'
version = '1.0.0'

sourceCompatibility = '11'
targetCompatibility = '11'

repositories { mavenCentral() }

dependencies {
    compileOnly 'com.morpheusdata:morpheus-plugin-api:0.15.4'
    compileOnly 'org.codehaus.groovy:groovy-all:3.0.11'
    compileOnly 'io.reactivex.rxjava3:rxjava:3.1.5'
    compileOnly 'org.slf4j:slf4j-api:1.7.26'
}

shadowJar {
    manifest {
        attributes(
            'Plugin-Class'  : 'com.example.myplugin.MyPlugin',
            'Plugin-Version': archiveVersion.get()
        )
    }
}

Enter fullscreen mode Exit fullscreen mode

compileOnly is not optional. Morpheus's runtime already provides these libraries; bundling them into your shadow JAR causes classpath conflicts that surface as NoSuchMethodError at load time.

Bumping the version field on every revision is worth doing — the version becomes part of the JAR filename, so you can confirm which build is loaded by glancing at the plugin list in the Morpheus UI.

Anatomy of a plugin

Three pieces.

The Plugin class is your entry point, registered in the JAR manifest as Plugin-Class. It registers your providers in initialize():

class MfaEnforcerPlugin extends Plugin {
    @Override String getCode() { 'morpheus-mfa-enforcer-plugin' }

    @Override void initialize() {
        setName("MFA Enforcer")
        def provider = new MfaEnforcerProvider(this, morpheus)
        pluginProviders.put(provider.code, provider)
    }
}

Enter fullscreen mode Exit fullscreen mode

The Provider class is where the work lives. Always extend the matching Abstract*Provider instead of implementing the interface directly. The abstract classes set up the renderer with the correct classpath prefix, register handlebars helpers (asset, nonce, i18n), and wire up things you don't want to wire up yourself.

@Slf4j
class MfaEnforcerProvider extends AbstractGlobalUIComponentProvider {
    Plugin plugin
    MorpheusContext morpheusContext

    MfaEnforcerProvider(Plugin plugin, MorpheusContext ctx) {
        this.plugin = plugin
        this.morpheusContext = ctx
    }

    @Override Boolean show(User user, Account account) {
        if (user == null || account == null) return false
        if (isMasterTenantAdmin(user, account)) return false
        return !isUsing2FA(user)
    }

    @Override HTMLResponse renderTemplate(User user, Account account) {
        def model = new ViewModel<>()
        model.object = [userSettingsUrl: '/user-settings']
        return getRenderer().renderTemplate("hbs/mfaEnforcer", model)
    }

    @Override MorpheusContext getMorpheus() { morpheusContext }
    @Override Plugin getPlugin() { plugin }
}

Enter fullscreen mode Exit fullscreen mode

The getMorpheus() and getPlugin() overrides are required by PluginProvider and need to be explicit because Groovy's auto-generated getters use the field name (getMorpheusContext, not getMorpheus).

Resources live under src/main/resources/. Handlebars templates go in renderer/hbs/. The renderer's classpath prefix is renderer, so getRenderer().renderTemplate("hbs/mfaEnforcer", model) resolves to src/main/resources/renderer/hbs/mfaEnforcer.hbs.

For inline <style> and <script> tags, use the {{nonce}} helper:

<div id="mfa-shield" role="dialog" aria-modal="true">
    <style nonce="{{nonce}}">
        #mfa-shield {
            position: fixed; inset: 0; z-index: 2147483647;
            background: rgba(15, 23, 42, 0.75);
            display: flex; align-items: center; justify-content: center;
        }
        /* ... */
    </style>
    <div class="card">
        <h1>Two-Factor Authentication Required</h1>
        <a class="cta" href="{{userSettingsUrl}}" target="_top">
            Go to User Settings → Enable 2FA
        </a>
    </div>
</div>

Enter fullscreen mode Exit fullscreen mode

Without the nonce, Morpheus's Content-Security-Policy header blocks the styles and the script silently. There's no console error, no log entry, no visible failure mode — the page just renders without your component. Easy hours to lose.

The example, end-to-end

A quick note on why this plugin exists, for the curious. Morpheus supports two-factor authentication out of the box, but only as an opt-in toggle on each user's profile there is no appliance wide setting to require it. The product documentation says so explicitly: "There is not currently a way for an administrator to enforce the use of two-factor authentication appliance wide." As an MSP we couldn't rely on every sub-tenant user enabling 2FA on their own, so we wrote a small plugin to close the gap.

MFA enforcer's full source is at emrbaykal/morpheus-plugin-test/morpheus-mfa-enforcer. It's roughly 200 lines split across four files:

morpheus-mfa-enforcer/
├── build.gradle                                          # project config
└── src/main/
    ├── groovy/com/morpheusdata/mfaenforcer/
    │   ├── MfaEnforcerPlugin.groovy                      # entry point, registers provider
    │   └── MfaEnforcerProvider.groovy                    # show() + renderTemplate() + state lookup
    └── resources/renderer/hbs/
        └── mfaEnforcer.hbs                               # overlay HTML + CSS + small JS

Enter fullscreen mode Exit fullscreen mode

The decision logic in show() reads the user's 2FA flag from the Morpheus database through MorpheusReportService.getReadOnlyDatabaseConnection(). The result is cached in a static ConcurrentHashMap with a short TTL — show() is hot, called on every authenticated page render, and uncached SELECTs would generate noticeable load and contention against Morpheus's own writes.

The visible result, in five steps:

A user logs in:

Login screen

The dashboard loads, but the overlay covers it immediately:

Dashboard with MFA overlay

The CTA hard-navigates to /user-settings, where the standard Morpheus 2FA enable flow runs (password confirmation, QR code, authenticator app):

Enable 2FA — confirm password

After enabling, the user-settings page reflects the new state:

User settings showing Disable 2FA / Get 2FA Code

On the next login, Morpheus prompts for the 2FA code from the authenticator:

2FA code prompt at login

And the dashboard loads cleanly the overlay is gone, because show() now reads is_using2fa = true:

Clean dashboard, no overlay

Build, package, deploy

./gradlew shadowJar
# build/libs/morpheus-mfa-enforcer-1.0.10-all.jar

Enter fullscreen mode Exit fullscreen mode

Then in the Morpheus UI: Administration → Integrations → Plugins → Choose File. Upload the shadow JAR. The plugin loads on every appliance node, no daemon restart required.

The plugin list in that section shows the version from the JAR filename. Combined with version-bump-on-every-revision, that's all the deployment tracking you need for a small plugin.

A few practical notes

A handful of things that aren't obvious from reading the SDK:

Always extend the matching Abstract*Provider. The abstract classes do non-trivial work — registering handlebars helpers, configuring the renderer's classpath prefix, wiring up locale support. Implementing the bare interface is reinventing this work, and the omissions tend to manifest as silent failures (template not found, CSS blocked by CSP) that are time-consuming to diagnose.

Be careful with hot-path DB access. Any provider invoked per page render needs caching. Without it you add latency to every authenticated request and, in the worst case, contend with Morpheus's own writes via InnoDB share locks. A small ConcurrentHashMap with a short TTL plus an explicit sql.commit() after each SELECT keeps the plugin a good citizen.

/api/* endpoints require an OAuth2 bearer token. Browser session cookies aren't accepted. If your plugin tries to fetch them from inline JavaScript in a template, you'll get 401. For state lookups, do them server-side via the database or via plugin-side service calls.

When something doesn't work, check the Morpheus daemon log first. Browser console second. Plugin failures often produce no console output at all — the symptoms are silent CSP blocks, silent SPA click intercepts, silent fail-open paths. The daemon log is more honest.

Closing

The plugin SDK is more capable than the documentation makes obvious. Once you have the three reference repositories cloned, the build environment set up, and the right Abstract*Provider class identified, the actual work moves quickly.

The same pattern applies to password complexity rules, IP-based access restrictions, role-aware session timeouts, and other guardrails the platform doesn't ship with: pick the provider that matches your hook, extend the abstract class, ship the shadow JAR.

If you've built something interesting with the SDK, drop a link in the comments.