4. ACL and extensions#

The clinic sample deliberately does not use @Permission — authorization is split between Spring Security (who is authenticated) and the palmyra-dbacl-mgmt extension (what each user is allowed to see and change). Password lifecycle is delegated to palmyra-dbpwd-mgmt. Exports are handled client-side, not by server handlers.

Wire Spring Security#

Permit the auth endpoints, authenticate everything else, and return 401 on auth failure so the frontend’s axios interceptor can redirect to login.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
          .csrf(AbstractHttpConfigurer::disable)
          .cors(Customizer.withDefaults())
          .authorizeHttpRequests(a -> a
              .requestMatchers("/auth/**").permitAll()
              .anyRequest().authenticated())
          .exceptionHandling(e -> e
              .authenticationEntryPoint((req, res, ex) ->
                  res.sendError(HttpServletResponse.SC_UNAUTHORIZED)))
          .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

The AuthProvider bridge#

Palmyra’s storage layer calls AuthProvider.getUser() to stamp audit columns and scope ACL lookups. Bridge it to Spring Security:

@Component
public class SpringSecurityUserProvider implements AuthProvider {
    @Override
    public String getUser() {
        var auth = SecurityContextHolder.getContext().getAuthentication();
        return (auth != null && auth.isAuthenticated()) ? auth.getName() : "anonymous";
    }
}

Your AbstractHandler already @Autowireds this, so every handler sees the current user for free.

ACL via palmyra-dbacl-mgmt#

The extension enforces per-operation, per-role ACLs at the filter chain — before any handler method runs. Wire it as a runtime dependency (done in step 1) and seed the ACL tables the extension owns:

  • acl_role — role names (ADMIN, PHARMACIST, VIEWER, …)
  • acl_permission — permission codes the UI consumes (e.g. CUTAP001 = "view manufacturers", CUTAP002 = "edit manufacturers")
  • acl_role_permission — many-to-many join
  • acl_user_role — user ↔ role assignments

Handlers return AclRights.ALL from aclCheck — the extension has already decided the user is allowed to touch the endpoint.

Because permission codes are data, the frontend reads the same codes from the server at login and gates UI controls on them (see the frontend tutorial). No @Permission annotations need to be maintained in parallel.

Password management via palmyra-dbpwd-mgmt#

The extension ships a /auth/** controller family covering: login, logout, change password, forgot password, reset token flows. It stores hashed passwords, tracks expiry, and integrates with the AuthProvider / PasswordEncoder above. To enable it, adding the dependency and declaring the relevant tables is enough — no bespoke controllers required.

Exports: client-side, not server-side#

The clinic sample does not publish CsvHandler / ExcelHandler endpoints. Its frontend builds XLSX in the browser from an already-loaded result page using xlsx + file-saver. Reasons this works for the project:

  • Exports are bounded (one screen of data, paginated).
  • The UI already has the rows rendered — no extra round-trip.
  • No server memory cost for infrequent exports.

If your use case demands server-side streaming exports (large datasets, scheduled jobs, access without UI), publish CsvHandler / ExcelHandler separately — the clinic pattern is a choice, not a limitation.

See also: AuthProvider, PreProcessor.