Skip to main content
Planned feature: This page describes the intended RBAC implementation for OrgStack. The current codebase includes Spring Security as a dependency but does not yet implement roles, permissions, or authorization logic.

Overview

OrgStack will implement Role-Based Access Control (RBAC) to manage what authenticated users can do within their organization. After authentication (proving who you are), authorization will determine what actions you’re allowed to perform.

RBAC fundamentals

OrgStack’s authorization model has three core concepts:

Users

Individual people who authenticate to the system

Roles

Named collections of permissions (e.g., ADMIN, MANAGER, USER)

Permissions

Specific actions users can perform (e.g., CREATE_USER, DELETE_PROJECT)

How authorization works

1

User authenticates

You log in and receive a JWT token containing your roles for your organization.
2

Request includes token

Your client includes the JWT in the Authorization header.
3

Spring Security extracts roles

The JWT filter parses your token and creates a SecurityContext with your roles.
4

Authorization check

Spring Security checks if your roles grant permission for the requested endpoint.
5

Access granted or denied

If authorized, the request proceeds. Otherwise, you receive 403 Forbidden.

Role hierarchy

OrgStack implements a hierarchical role structure where higher roles inherit permissions from lower roles:
SYSTEM_ADMIN (platform-level, across all tenants)
    |
    └─ ORGANIZATION_ADMIN (full control within organization)
           |
           ├─ MANAGER (manage users and projects)
           │      |
           │      └─ USER (basic access)
           │             |
           │             └─ GUEST (read-only)

           └─ AUDITOR (read-only across all data)
Roles are scoped to organizations. An ADMIN in Organization A has no privileges in Organization B. The only exception is SYSTEM_ADMIN, which should be used sparingly.

Protecting endpoints

Spring Security provides multiple ways to protect your REST endpoints:

Method security

Use annotations on controller methods:
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping
    @PreAuthorize("hasRole('USER')")
    public List<User> listUsers() {
        // All authenticated users can list users in their org
    }
    
    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public User createUser(@RequestBody CreateUserRequest request) {
        // Only admins can create users
    }
    
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') and #id != authentication.principal.id")
    public void deleteUser(@PathVariable UUID id) {
        // Admins can delete users, but not themselves
    }
}

Path-based security

Configure security rules in your security configuration:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/**").hasRole("USER")
                .anyRequest().authenticated()
            );
        return http.build();
    }
}
Method-level security with @PreAuthorize is more flexible and easier to test than path-based rules. Use it for fine-grained authorization logic.

Permission checks in services

Authorization shouldn’t stop at the controller layer. Service methods should also validate permissions:
@Service
public class ProjectService {
    
    public Project updateProject(UUID projectId, UpdateProjectRequest request) {
        Project project = projectRepository.findById(projectId)
            .orElseThrow(() -> new NotFoundException("Project not found"));
        
        // Verify the project belongs to the current user's organization
        UUID currentOrgId = SecurityContext.getCurrentOrganizationId();
        if (!project.getOrganizationId().equals(currentOrgId)) {
            throw new ForbiddenException("Cannot access project from different organization");
        }
        
        // Verify the user has permission to update this project
        if (!SecurityContext.hasRole("ADMIN") && 
            !project.getOwnerId().equals(SecurityContext.getCurrentUserId())) {
            throw new ForbiddenException("Only project owners or admins can update projects");
        }
        
        // Proceed with update
        return projectRepository.save(project);
    }
}

Organization-scoped roles

Roles in OrgStack are always scoped to an organization. The database schema enforces this:
CREATE TABLE user_roles (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id),
    organization_id UUID NOT NULL REFERENCES organizations(id),
    role VARCHAR(50) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(user_id, organization_id, role)
);
A user can have different roles in different organizations. For example, you might be an ADMIN in Organization A and a USER in Organization B.

Custom permissions

Beyond predefined roles, you can implement custom permissions for fine-grained control:
public enum Permission {
    // User management
    USER_CREATE,
    USER_READ,
    USER_UPDATE,
    USER_DELETE,
    
    // Project management
    PROJECT_CREATE,
    PROJECT_READ,
    PROJECT_UPDATE,
    PROJECT_DELETE,
    
    // Billing
    BILLING_READ,
    BILLING_MANAGE,
    
    // Admin
    ORG_SETTINGS_MANAGE,
    ROLE_ASSIGN
}
Map permissions to roles:
public enum Role {
    ADMIN(Permission.values()), // All permissions
    MANAGER(Permission.USER_READ, Permission.USER_UPDATE, 
            Permission.PROJECT_CREATE, Permission.PROJECT_READ,
            Permission.PROJECT_UPDATE),
    USER(Permission.PROJECT_READ, Permission.USER_READ),
    GUEST(Permission.PROJECT_READ);
    
    private final Set<Permission> permissions;
}

Testing authorization

Spring Security provides test utilities for verifying authorization rules:
@SpringBootTest
@AutoConfigureMockMvc
class AuthorizationTests {
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    @WithMockUser(roles = "USER")
    void userCannotAccessAdminEndpoint() throws Exception {
        mockMvc.perform(delete("/api/users/123"))
            .andExpect(status().isForbidden());
    }
    
    @Test
    @WithMockUser(roles = "ADMIN")
    void adminCanDeleteUsers() throws Exception {
        mockMvc.perform(delete("/api/users/123"))
            .andExpect(status().isOk());
    }
}

Audit logging

OrgStack automatically tracks when entities are created and modified using JPA auditing:
BaseEntity.java
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public class BaseEntity {
    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant createdAt;
    
    @LastModifiedDate
    @Column(nullable = false)
    private Instant updatedAt;
}
To track who performed actions, extend this with user auditing:
@CreatedBy
@Column(nullable = false, updatable = false)
private UUID createdBy;

@LastModifiedBy
@Column(nullable = false)
private UUID lastModifiedBy;
Implement AuditorAware<UUID> to automatically populate createdBy and lastModifiedBy from the current security context.

Best practices

1

Fail securely

When in doubt, deny access. It’s better to be too restrictive than too permissive.
2

Check at multiple layers

Validate authorization at both controller and service layers. Defense in depth prevents security gaps.
3

Use explicit role checks

Avoid generic isAuthenticated() checks. Be specific about which roles can perform which actions.
4

Test negative cases

Write tests that verify unauthorized users are blocked, not just that authorized users succeed.
5

Audit sensitive operations

Log all administrative actions (user creation, role changes, deletions) for compliance and security.

Common authorization patterns

Users can only modify resources they own, unless they’re admins:
@PreAuthorize("hasRole('ADMIN') or @projectSecurity.isOwner(#projectId)")
Users can only access resources within their organization:
if (!resource.getOrganizationId().equals(getCurrentOrgId())) {
    throw new ForbiddenException();
}
Enable features based on organization plan or user role:
@PreAuthorize("@featureService.isEnabled('ADVANCED_ANALYTICS', #orgId)")
Restrict access based on user account status or trial expiration:
if (user.getAccountStatus() != AccountStatus.ACTIVE) {
    throw new AccountInactiveException();
}

Next steps

Authentication

Learn how users prove their identity

Architecture

Understand how authorization fits in the layered architecture

Build docs developers (and LLMs) love